Skip to content

Commit ac398ec

Browse files
authored
feat/fix: Configuration enhancements - embed forServer field in FlagConfigResponse serialized form (#43)
* store `forServer` in JSON bytes
1 parent e3fc4df commit ac398ec

File tree

5 files changed

+137
-24
lines changed

5 files changed

+137
-24
lines changed

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ plugins {
66
}
77

88
group = 'cloud.eppo'
9-
version = '3.2.0-SNAPSHOT'
9+
version = '3.3.0-SNAPSHOT'
1010
ext.isReleaseVersion = !version.endsWith("SNAPSHOT")
1111

1212
java {

src/main/java/cloud/eppo/api/Configuration.java

Lines changed: 61 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@
44

55
import cloud.eppo.ufc.dto.*;
66
import cloud.eppo.ufc.dto.adapters.EppoModule;
7+
import com.fasterxml.jackson.databind.JsonNode;
78
import com.fasterxml.jackson.databind.ObjectMapper;
8-
import java.io.IOException;
9+
import com.fasterxml.jackson.databind.node.ObjectNode;
10+
import java.io.*;
911
import java.util.Collections;
1012
import java.util.Map;
1113
import java.util.Set;
1214
import java.util.stream.Collectors;
15+
import org.jetbrains.annotations.Nullable;
1316
import org.slf4j.Logger;
1417
import org.slf4j.LoggerFactory;
1518

@@ -46,6 +49,9 @@
4649
* then check `requiresBanditModels()`.
4750
*/
4851
public class Configuration {
52+
private static final ObjectMapper mapper =
53+
new ObjectMapper().registerModule(EppoModule.eppoModule());
54+
4955
private static final Logger log = LoggerFactory.getLogger(Configuration.class);
5056
private final Map<String, BanditReference> banditReferences;
5157
private final Map<String, FlagConfig> flags;
@@ -68,6 +74,17 @@ private Configuration(
6874
this.banditReferences = banditReferences;
6975
this.bandits = bandits;
7076
this.isConfigObfuscated = isConfigObfuscated;
77+
78+
// Graft the `forServer` boolean into the flagConfigJson'
79+
if (flagConfigJson != null && flagConfigJson.length != 0) {
80+
try {
81+
JsonNode jNode = mapper.readTree(flagConfigJson);
82+
((ObjectNode) jNode).put("forServer", !isConfigObfuscated);
83+
flagConfigJson = mapper.writeValueAsBytes(jNode);
84+
} catch (IOException e) {
85+
log.error("Error adding `forServer` field to FlagConfigResponse JSON");
86+
}
87+
}
7188
this.flagConfigJson = flagConfigJson;
7289
this.banditParamsJson = banditParamsJson;
7390
}
@@ -123,6 +140,10 @@ public byte[] serializeBanditParamsToBytes() {
123140
return banditParamsJson;
124141
}
125142

143+
public boolean isEmpty() {
144+
return flags == null || flags.isEmpty();
145+
}
146+
126147
public static Builder builder(byte[] flagJson, boolean isConfigObfuscated) {
127148
return new Builder(flagJson, isConfigObfuscated);
128149
}
@@ -132,44 +153,59 @@ public static Builder builder(byte[] flagJson, boolean isConfigObfuscated) {
132153
* @see Configuration for usage.
133154
*/
134155
public static class Builder {
135-
private static final ObjectMapper mapper =
136-
new ObjectMapper().registerModule(EppoModule.eppoModule());
137156

138157
private final boolean isConfigObfuscated;
139158
private final Map<String, FlagConfig> flags;
140-
private Map<String, BanditReference> banditReferences;
159+
private final Map<String, BanditReference> banditReferences;
141160
private Map<String, BanditParameters> bandits = Collections.emptyMap();
142161
private final byte[] flagJson;
143162
private byte[] banditParamsJson;
144163

145-
public Builder(String flagJson, boolean isConfigObfuscated) {
146-
this(flagJson.getBytes(), isConfigObfuscated);
147-
}
148-
149-
public Builder(byte[] flagJson, boolean isConfigObfuscated) {
150-
this.isConfigObfuscated = isConfigObfuscated;
151-
164+
private static FlagConfigResponse parseFlagResponse(byte[] flagJson) {
152165
if (flagJson == null || flagJson.length == 0) {
153-
throw new RuntimeException(
154-
"Null or empty configuration string. Call `Configuration.Empty()` instead");
166+
log.warn("Null or empty configuration string. Call `Configuration.Empty()` instead");
167+
return null;
155168
}
156-
157-
// Build the flags config from the json string.
158169
FlagConfigResponse config;
159170
try {
160-
config = mapper.readValue(flagJson, FlagConfigResponse.class);
171+
return mapper.readValue(flagJson, FlagConfigResponse.class);
161172
} catch (IOException e) {
162173
throw new RuntimeException(e);
163174
}
175+
}
176+
177+
public Builder(String flagJson, boolean isConfigObfuscated) {
178+
this(flagJson.getBytes(), parseFlagResponse(flagJson.getBytes()), isConfigObfuscated);
179+
}
180+
181+
public Builder(byte[] flagJson, boolean isConfigObfuscated) {
182+
this(flagJson, parseFlagResponse(flagJson), isConfigObfuscated);
183+
}
184+
185+
public Builder(byte[] flagJson, FlagConfigResponse flagConfigResponse) {
186+
this(flagJson, flagConfigResponse, !flagConfigResponse.isForServer());
187+
}
164188

165-
if (config == null || config.getFlags() == null) {
189+
/** Use this constructor when the FlagConfigResponse has the `forServer` field populated. */
190+
public Builder(byte[] flagJson) {
191+
this(flagJson, parseFlagResponse(flagJson));
192+
}
193+
194+
public Builder(
195+
byte[] flagJson,
196+
@Nullable FlagConfigResponse flagConfigResponse,
197+
boolean isConfigObfuscated) {
198+
this.isConfigObfuscated = isConfigObfuscated;
199+
this.flagJson = flagJson;
200+
if (flagConfigResponse == null
201+
|| flagConfigResponse.getFlags() == null
202+
|| flagConfigResponse.getFlags().isEmpty()) {
166203
log.warn("'flags' map missing in flag definition JSON");
167204
flags = Collections.emptyMap();
168-
this.flagJson = null;
205+
banditReferences = Collections.emptyMap();
169206
} else {
170-
flags = Collections.unmodifiableMap(config.getFlags());
171-
banditReferences = Collections.unmodifiableMap(config.getBanditReferences());
172-
this.flagJson = flagJson;
207+
flags = Collections.unmodifiableMap(flagConfigResponse.getFlags());
208+
banditReferences = Collections.unmodifiableMap(flagConfigResponse.getBanditReferences());
173209
log.debug("Loaded {} flag definitions from flag definition JSON", flags.size());
174210
}
175211
}
@@ -206,6 +242,10 @@ public Builder banditParameters(String banditParameterJson) {
206242
}
207243

208244
public Builder banditParameters(byte[] banditParameterJson) {
245+
if (banditParameterJson == null || banditParameterJson.length == 0) {
246+
log.debug("Bandit parameters are null or empty");
247+
return this;
248+
}
209249
BanditParametersResponse config;
210250
try {
211251
config = mapper.readValue(banditParameterJson, BanditParametersResponse.class);

src/main/java/cloud/eppo/ufc/dto/FlagConfigResponse.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,24 @@
66
public class FlagConfigResponse {
77
private final Map<String, FlagConfig> flags;
88
private final Map<String, BanditReference> banditReferences;
9+
private final boolean forServer;
910

1011
public FlagConfigResponse(
1112
Map<String, FlagConfig> flags, Map<String, BanditReference> banditReferences) {
13+
this(flags, banditReferences, false);
14+
}
15+
16+
public FlagConfigResponse(
17+
Map<String, FlagConfig> flags,
18+
Map<String, BanditReference> banditReferences,
19+
boolean isConfigObfuscated) {
1220
this.flags = flags;
1321
this.banditReferences = banditReferences;
22+
this.forServer = !isConfigObfuscated;
1423
}
1524

1625
public FlagConfigResponse() {
17-
this(new ConcurrentHashMap<>(), new ConcurrentHashMap<>());
26+
this(new ConcurrentHashMap<>(), new ConcurrentHashMap<>(), false);
1827
}
1928

2029
public Map<String, FlagConfig> getFlags() {
@@ -24,4 +33,8 @@ public Map<String, FlagConfig> getFlags() {
2433
public Map<String, BanditReference> getBanditReferences() {
2534
return this.banditReferences;
2635
}
36+
37+
public boolean isForServer() {
38+
return this.forServer;
39+
}
2740
}

src/main/java/cloud/eppo/ufc/dto/adapters/FlagConfigResponseDeserializer.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ public FlagConfigResponse deserialize(JsonParser jp, DeserializationContext ctxt
4848
return new FlagConfigResponse();
4949
}
5050

51+
// Default is to assume that the config is not obfuscated.
52+
JsonNode forServerNode = rootNode.get("forServer");
53+
boolean isConfigObfuscated = forServerNode != null && !forServerNode.asBoolean();
54+
5155
Map<String, FlagConfig> flags = new ConcurrentHashMap<>();
5256

5357
flagsNode
@@ -74,7 +78,7 @@ public FlagConfigResponse deserialize(JsonParser jp, DeserializationContext ctxt
7478
}
7579
}
7680

77-
return new FlagConfigResponse(flags, banditReferences);
81+
return new FlagConfigResponse(flags, banditReferences, isConfigObfuscated);
7882
}
7983

8084
private FlagConfig deserializeFlag(JsonNode jsonNode) {
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package cloud.eppo.api;
2+
3+
import static org.junit.jupiter.api.Assertions.assertFalse;
4+
import static org.junit.jupiter.api.Assertions.assertTrue;
5+
6+
import cloud.eppo.ufc.dto.FlagConfigResponse;
7+
import cloud.eppo.ufc.dto.adapters.EppoModule;
8+
import com.fasterxml.jackson.databind.ObjectMapper;
9+
import java.io.IOException;
10+
import org.junit.jupiter.api.Test;
11+
12+
public class ConfigurationBuilderTest {
13+
14+
private static final ObjectMapper mapper =
15+
new ObjectMapper().registerModule(EppoModule.eppoModule());
16+
17+
@Test
18+
public void testHydrateConfigFromBytesForServer_true() {
19+
byte[] jsonBytes = "{ \"forServer\": true, \"flags\":{} }".getBytes();
20+
Configuration config = new Configuration.Builder(jsonBytes).build();
21+
assertFalse(config.isConfigObfuscated());
22+
}
23+
24+
@Test
25+
public void testHydrateConfigFromBytesForServer_false() {
26+
byte[] jsonBytes = "{ \"forServer\": false, \"flags\":{} }".getBytes();
27+
Configuration config = new Configuration.Builder(jsonBytes).build();
28+
assertTrue(config.isConfigObfuscated());
29+
}
30+
31+
@Test
32+
public void testBuildConfigAddsForServer_true() throws IOException {
33+
byte[] jsonBytes = "{ \"flags\":{} }".getBytes();
34+
Configuration config = Configuration.builder(jsonBytes, false).build();
35+
assertFalse(config.isConfigObfuscated());
36+
37+
byte[] serializedFlags = config.serializeFlagConfigToBytes();
38+
FlagConfigResponse rehydratedConfig =
39+
mapper.readValue(serializedFlags, FlagConfigResponse.class);
40+
41+
assertTrue(rehydratedConfig.isForServer());
42+
}
43+
44+
@Test
45+
public void testBuildConfigAddsForServer_false() throws IOException {
46+
byte[] jsonBytes = "{ \"flags\":{} }".getBytes();
47+
Configuration config = Configuration.builder(jsonBytes, true).build();
48+
assertTrue(config.isConfigObfuscated());
49+
50+
byte[] serializedFlags = config.serializeFlagConfigToBytes();
51+
FlagConfigResponse rehydratedConfig =
52+
mapper.readValue(serializedFlags, FlagConfigResponse.class);
53+
54+
assertFalse(rehydratedConfig.isForServer());
55+
}
56+
}

0 commit comments

Comments
 (0)