Skip to content

Commit 239d877

Browse files
fayyazarshadzashraf1985
authored andcommitted
feat: getOptimizelyConfig API to get static experiments and features data (#348)
## Summary Added `OptimizelyConfigService` which generates `OptimizelyConfig` Object containing experiments and features data. This is the first step in the development of `getOptimizelyConfig` api. This service always generates a new `OptimzelyConfig` object for now. Next PR will contain caching implementation for `OptimizelyConfig` object. ## Test plan Added Unit Tests
1 parent c656e37 commit 239d877

13 files changed

+1337
-1
lines changed

core-api/src/main/java/com/optimizely/ab/Optimizely.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/****************************************************************************
2-
* Copyright 2016-2019, Optimizely, Inc. and contributors *
2+
* Copyright 2016-2020, Optimizely, Inc. and contributors *
33
* *
44
* Licensed under the Apache License, Version 2.0 (the "License"); *
55
* you may not use this file except in compliance with the License. *
@@ -28,6 +28,8 @@
2828
import com.optimizely.ab.event.internal.*;
2929
import com.optimizely.ab.event.internal.payload.EventBatch;
3030
import com.optimizely.ab.notification.*;
31+
import com.optimizely.ab.optimizelyconfig.OptimizelyConfig;
32+
import com.optimizely.ab.optimizelyconfig.OptimizelyConfigService;
3133
import org.slf4j.Logger;
3234
import org.slf4j.LoggerFactory;
3335

@@ -882,6 +884,20 @@ public Variation getForcedVariation(@Nonnull String experimentKey,
882884
return decisionService.getForcedVariation(experiment, userId);
883885
}
884886

887+
/**
888+
* Get {@link OptimizelyConfig} containing experiments and features map
889+
*
890+
* @return {@link OptimizelyConfig}
891+
*/
892+
public OptimizelyConfig getOptimizelyConfig() {
893+
ProjectConfig projectConfig = getProjectConfig();
894+
if (projectConfig == null) {
895+
logger.error("Optimizely instance is not valid, failing getOptimizelyConfig call.");
896+
return null;
897+
}
898+
return new OptimizelyConfigService(projectConfig).getConfig();
899+
}
900+
885901
/**
886902
* @return the current {@link ProjectConfig} instance.
887903
*/
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/****************************************************************************
2+
* Copyright 2020, Optimizely, Inc. and contributors *
3+
* *
4+
* Licensed under the Apache License, Version 2.0 (the "License"); *
5+
* you may not use this file except in compliance with the License. *
6+
* You may obtain a copy of the License at *
7+
* *
8+
* http://www.apache.org/licenses/LICENSE-2.0 *
9+
* *
10+
* Unless required by applicable law or agreed to in writing, software *
11+
* distributed under the License is distributed on an "AS IS" BASIS, *
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13+
* See the License for the specific language governing permissions and *
14+
* limitations under the License. *
15+
***************************************************************************/
16+
package com.optimizely.ab.optimizelyconfig;
17+
18+
import java.util.*;
19+
20+
/**
21+
* Interface for OptimizleyConfig
22+
*/
23+
public class OptimizelyConfig {
24+
25+
private Map<String, OptimizelyExperiment> experimentsMap;
26+
private Map<String, OptimizelyFeature> featuresMap;
27+
private String revision;
28+
29+
public OptimizelyConfig(Map<String, OptimizelyExperiment> experimentsMap,
30+
Map<String, OptimizelyFeature> featuresMap,
31+
String revision) {
32+
this.experimentsMap = experimentsMap;
33+
this.featuresMap = featuresMap;
34+
this.revision = revision;
35+
}
36+
37+
public Map<String, OptimizelyExperiment> getExperimentsMap() {
38+
return experimentsMap;
39+
}
40+
41+
public Map<String, OptimizelyFeature> getFeaturesMap() {
42+
return featuresMap;
43+
}
44+
45+
public String getRevision() {
46+
return revision;
47+
}
48+
}
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/****************************************************************************
2+
* Copyright 2020, Optimizely, Inc. and contributors *
3+
* *
4+
* Licensed under the Apache License, Version 2.0 (the "License"); *
5+
* you may not use this file except in compliance with the License. *
6+
* You may obtain a copy of the License at *
7+
* *
8+
* http://www.apache.org/licenses/LICENSE-2.0 *
9+
* *
10+
* Unless required by applicable law or agreed to in writing, software *
11+
* distributed under the License is distributed on an "AS IS" BASIS, *
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13+
* See the License for the specific language governing permissions and *
14+
* limitations under the License. *
15+
***************************************************************************/
16+
package com.optimizely.ab.optimizelyconfig;
17+
18+
import com.optimizely.ab.annotations.VisibleForTesting;
19+
import com.optimizely.ab.config.*;
20+
import java.util.*;
21+
import java.util.function.Function;
22+
import java.util.stream.Collectors;
23+
24+
public class OptimizelyConfigService {
25+
26+
private ProjectConfig projectConfig;
27+
private OptimizelyConfig optimizelyConfig;
28+
29+
public OptimizelyConfigService(ProjectConfig projectConfig) {
30+
this.projectConfig = projectConfig;
31+
32+
Map<String, OptimizelyExperiment> experimentsMap = getExperimentsMap();
33+
optimizelyConfig = new OptimizelyConfig(
34+
experimentsMap,
35+
getFeaturesMap(experimentsMap),
36+
projectConfig.getRevision()
37+
);
38+
}
39+
40+
/**
41+
* returns maps for experiment and features to be returned as one object
42+
*
43+
* @return {@link OptimizelyConfig} containing experiments and features
44+
*/
45+
public OptimizelyConfig getConfig() {
46+
return optimizelyConfig;
47+
}
48+
49+
/**
50+
* Generates a Map which contains list of variables for each feature key.
51+
* This map is used for merging variation and feature variables.
52+
*/
53+
@VisibleForTesting
54+
Map<String, List<FeatureVariable>> generateFeatureKeyToVariablesMap() {
55+
List<FeatureFlag> featureFlags = projectConfig.getFeatureFlags();
56+
if (featureFlags == null) {
57+
return Collections.emptyMap();
58+
}
59+
return featureFlags.stream().collect(Collectors.toMap(FeatureFlag::getKey, featureFlag -> featureFlag.getVariables()));
60+
}
61+
62+
@VisibleForTesting
63+
String getExperimentFeatureKey(String experimentId) {
64+
List<String> featureKeys = projectConfig.getExperimentFeatureKeyMapping().get(experimentId);
65+
return featureKeys != null ? featureKeys.get(0) : null;
66+
}
67+
68+
@VisibleForTesting
69+
Map<String, OptimizelyExperiment> getExperimentsMap() {
70+
List<Experiment> experiments = projectConfig.getExperiments();
71+
if(experiments == null) {
72+
return Collections.emptyMap();
73+
}
74+
return experiments.stream().collect(Collectors.toMap(Experiment::getKey, experiment -> new OptimizelyExperiment(
75+
experiment.getId(),
76+
experiment.getKey(),
77+
getVariationsMap(experiment.getVariations(), experiment.getId())
78+
)));
79+
}
80+
81+
@VisibleForTesting
82+
Map<String, OptimizelyVariation> getVariationsMap(List<Variation> variations, String experimentId) {
83+
if(variations == null) {
84+
return Collections.emptyMap();
85+
}
86+
Boolean isFeatureExperiment = this.getExperimentFeatureKey(experimentId) != null;
87+
return variations.stream().collect(Collectors.toMap(Variation::getKey, variation -> new OptimizelyVariation(
88+
variation.getId(),
89+
variation.getKey(),
90+
isFeatureExperiment ? variation.getFeatureEnabled() : null,
91+
getMergedVariablesMap(variation, experimentId)
92+
)));
93+
}
94+
95+
/**
96+
* Merges Additional information from variables in feature flag with variation variables as per the following logic.
97+
* 1. If Variation has variables and feature is enabled, then only `type` and `key` are merged from feature variable.
98+
* 2. If Variation has variables and feature is disabled, then `type` and `key` are merged and `defaultValue` of feature variable is merged as `value` of variation variable.
99+
* 3. If Variation does not contain a variable, then all `id`, `key`, `type` and defaultValue as `value` is used from feature varaible and added to variation.
100+
*/
101+
@VisibleForTesting
102+
Map<String, OptimizelyVariable> getMergedVariablesMap(Variation variation, String experimentId) {
103+
String featureKey = this.getExperimentFeatureKey(experimentId);
104+
if (featureKey != null) {
105+
// Map containing variables list for every feature key used for merging variation and feature variables.
106+
Map<String, List<FeatureVariable>> featureKeyToVariablesMap = generateFeatureKeyToVariablesMap();
107+
108+
// Generate temp map of all the available variable values from variation.
109+
Map<String, OptimizelyVariable> tempVariableIdMap = getFeatureVariableUsageInstanceMap(variation.getFeatureVariableUsageInstances());
110+
111+
// Iterate over all the variables available in associated feature.
112+
// Use value from variation variable if variable is available in variation and feature is enabled, otherwise use defaultValue from feature variable.
113+
List<FeatureVariable> featureVariables = featureKeyToVariablesMap.get(featureKey);
114+
if (featureVariables == null) {
115+
return Collections.emptyMap();
116+
}
117+
118+
return featureVariables.stream().collect(Collectors.toMap(FeatureVariable::getKey, featureVariable -> new OptimizelyVariable(
119+
featureVariable.getId(),
120+
featureVariable.getKey(),
121+
featureVariable.getType().getVariableType().toLowerCase(),
122+
variation.getFeatureEnabled() && tempVariableIdMap.get(featureVariable.getId()) != null
123+
? tempVariableIdMap.get(featureVariable.getId()).getValue()
124+
: featureVariable.getDefaultValue()
125+
)));
126+
}
127+
return Collections.emptyMap();
128+
}
129+
130+
@VisibleForTesting
131+
Map<String, OptimizelyVariable> getFeatureVariableUsageInstanceMap(List<FeatureVariableUsageInstance> featureVariableUsageInstances) {
132+
if(featureVariableUsageInstances == null) {
133+
return Collections.emptyMap();
134+
}
135+
return featureVariableUsageInstances.stream().collect(Collectors.toMap(FeatureVariableUsageInstance::getId, variable -> new OptimizelyVariable(
136+
variable.getId(),
137+
null,
138+
null,
139+
variable.getValue()
140+
)));
141+
}
142+
143+
@VisibleForTesting
144+
Map<String, OptimizelyFeature> getFeaturesMap(Map<String, OptimizelyExperiment> allExperimentsMap) {
145+
List<FeatureFlag> featureFlags = projectConfig.getFeatureFlags();
146+
if(featureFlags == null) {
147+
return Collections.emptyMap();
148+
}
149+
return featureFlags.stream().collect(Collectors.toMap(FeatureFlag::getKey, featureFlag -> new OptimizelyFeature(
150+
featureFlag.getId(),
151+
featureFlag.getKey(),
152+
getExperimentsMapForFeature(featureFlag.getExperimentIds(), allExperimentsMap),
153+
getFeatureVariablesMap(featureFlag.getVariables())
154+
)));
155+
}
156+
157+
@VisibleForTesting
158+
Map<String, OptimizelyExperiment> getExperimentsMapForFeature(List<String> experimentIds, Map<String, OptimizelyExperiment> allExperimentsMap) {
159+
if (experimentIds == null) {
160+
return Collections.emptyMap();
161+
}
162+
163+
List<String> experimentKeys = experimentIds.stream()
164+
.map(experimentId -> projectConfig.getExperimentIdMapping().get(experimentId).getKey())
165+
.collect(Collectors.toList());
166+
167+
return experimentKeys.stream().collect(Collectors.toMap(Function.identity(), key -> allExperimentsMap.get(key)));
168+
}
169+
170+
@VisibleForTesting
171+
Map<String, OptimizelyVariable> getFeatureVariablesMap(List<FeatureVariable> featureVariables) {
172+
if (featureVariables == null) {
173+
return Collections.emptyMap();
174+
}
175+
return featureVariables.stream().collect(Collectors.toMap(FeatureVariable::getKey, featureVariable -> new OptimizelyVariable(
176+
featureVariable.getId(),
177+
featureVariable.getKey(),
178+
featureVariable.getType().getVariableType().toLowerCase(),
179+
featureVariable.getDefaultValue()
180+
)));
181+
}
182+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/****************************************************************************
2+
* Copyright 2020, Optimizely, Inc. and contributors *
3+
* *
4+
* Licensed under the Apache License, Version 2.0 (the "License"); *
5+
* you may not use this file except in compliance with the License. *
6+
* You may obtain a copy of the License at *
7+
* *
8+
* http://www.apache.org/licenses/LICENSE-2.0 *
9+
* *
10+
* Unless required by applicable law or agreed to in writing, software *
11+
* distributed under the License is distributed on an "AS IS" BASIS, *
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13+
* See the License for the specific language governing permissions and *
14+
* limitations under the License. *
15+
***************************************************************************/
16+
package com.optimizely.ab.optimizelyconfig;
17+
18+
import com.optimizely.ab.config.IdKeyMapped;
19+
20+
import java.util.Map;
21+
22+
/**
23+
* Represents the experiment's map in {@link OptimizelyConfig}
24+
*/
25+
public class OptimizelyExperiment implements IdKeyMapped {
26+
27+
private String id;
28+
private String key;
29+
private Map<String, OptimizelyVariation> variationsMap;
30+
31+
public OptimizelyExperiment(String id, String key, Map<String, OptimizelyVariation> variationsMap) {
32+
this.id = id;
33+
this.key = key;
34+
this.variationsMap = variationsMap;
35+
}
36+
37+
public String getId() {
38+
return id;
39+
}
40+
41+
public String getKey() {
42+
return key;
43+
}
44+
45+
public Map<String, OptimizelyVariation> getVariationsMap() {
46+
return variationsMap;
47+
}
48+
49+
@Override
50+
public boolean equals(Object obj) {
51+
if (obj == null || getClass() != obj.getClass()) return false;
52+
if (obj == this) return true;
53+
OptimizelyExperiment optimizelyExperiment = (OptimizelyExperiment) obj;
54+
return id.equals(optimizelyExperiment.getId()) &&
55+
key.equals(optimizelyExperiment.getKey()) &&
56+
variationsMap.equals(optimizelyExperiment.getVariationsMap());
57+
}
58+
59+
@Override
60+
public int hashCode() {
61+
int hash = id.hashCode();
62+
hash = 31 * hash + variationsMap.hashCode();
63+
return hash;
64+
}
65+
}

0 commit comments

Comments
 (0)