Skip to content
This repository was archived by the owner on May 30, 2024. It is now read-only.

Commit c183da7

Browse files
prepare 5.4.1 release (#236)
1 parent fe3771b commit c183da7

File tree

3 files changed

+195
-29
lines changed

3 files changed

+195
-29
lines changed

src/main/java/com/launchdarkly/sdk/server/Components.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -204,9 +204,6 @@ public static PollingDataSourceBuilder pollingDataSource() {
204204
* .dataStore(Components.persistentDataStore(Redis.dataStore())) // assuming the Relay Proxy is using Redis
205205
* .build();
206206
* </code></pre>
207-
* <p>
208-
* (Note that the interface is still named {@link DataSourceFactory}, but in a future version it
209-
* will be renamed to {@code DataSourceFactory}.)
210207
*
211208
* @return a factory object
212209
* @since 4.12.0

src/main/java/com/launchdarkly/sdk/server/EvaluatorPreprocessing.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ static void preprocessFlag(FeatureFlag f) {
6161
for (int i = 0; i < n; i++) {
6262
preprocessFlagRule(rules.get(i), i);
6363
}
64+
preprocessValueList(f.getVariations());
6465
}
6566

6667
static void preprocessSegment(Segment s) {
@@ -92,6 +93,13 @@ static void preprocessSegmentRule(SegmentRule r, int ruleIndex) {
9293
}
9394

9495
static void preprocessClause(Clause c) {
96+
// If the clause values contain a null (which is valid in terms of the JSON schema, even if it
97+
// can't ever produce a true result), Gson will give us an actual null. Change this to
98+
// LDValue.ofNull() to avoid NPEs down the line. It's more efficient to do this just once at
99+
// deserialization time than to do it in every clause match.
100+
List<LDValue> values = c.getValues();
101+
preprocessValueList(values);
102+
95103
Operator op = c.getOp();
96104
if (op == null) {
97105
return;
@@ -102,7 +110,6 @@ static void preprocessClause(Clause c) {
102110
// clause values. Converting the value list to a Set allows us to do a fast lookup instead of
103111
// a linear search. We do not do this for other operators (or if there are fewer than two
104112
// values) because the slight extra overhead of a Set is not worthwhile in those case.
105-
List<LDValue> values = c.getValues();
106113
if (values.size() > 1) {
107114
c.setPreprocessed(new ClauseExtra(ImmutableSet.copyOf(values), null));
108115
}
@@ -130,6 +137,18 @@ static void preprocessClause(Clause c) {
130137
}
131138
}
132139

140+
static void preprocessValueList(List<LDValue> values) {
141+
// If a list of values contains a null (which is valid in terms of the JSON schema, even if it
142+
// isn't useful because the SDK considers this a non-value), Gson will give us an actual null.
143+
// Change this to LDValue.ofNull() to avoid NPEs down the line. It's more efficient to do this
144+
// just once at deserialization time than to do it in every clause match.
145+
for (int i = 0; i < values.size(); i++) {
146+
if (values.get(i) == null) {
147+
values.set(i, LDValue.ofNull());
148+
}
149+
}
150+
}
151+
133152
private static ClauseExtra preprocessClauseValues(
134153
List<LDValue> values,
135154
Function<LDValue, ClauseExtra.ValueExtra> f

src/test/java/com/launchdarkly/sdk/server/DataModelSerializationTest.java

Lines changed: 175 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,24 @@
33
import com.google.common.collect.ImmutableList;
44
import com.google.common.collect.ImmutableSet;
55
import com.launchdarkly.sdk.LDValue;
6+
import com.launchdarkly.sdk.ObjectBuilder;
67
import com.launchdarkly.sdk.UserAttribute;
78
import com.launchdarkly.sdk.server.DataModel.Clause;
89
import com.launchdarkly.sdk.server.DataModel.FeatureFlag;
910
import com.launchdarkly.sdk.server.DataModel.Operator;
11+
import com.launchdarkly.sdk.server.DataModel.Prerequisite;
1012
import com.launchdarkly.sdk.server.DataModel.Rule;
1113
import com.launchdarkly.sdk.server.DataModel.Segment;
1214
import com.launchdarkly.sdk.server.DataModel.SegmentRule;
1315
import com.launchdarkly.sdk.server.DataModel.Target;
16+
import com.launchdarkly.sdk.server.DataModel.WeightedVariation;
1417
import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor;
1518

1619
import org.junit.Test;
1720

21+
import java.util.Collections;
22+
import java.util.function.Consumer;
23+
1824
import static com.launchdarkly.sdk.server.DataModel.FEATURES;
1925
import static com.launchdarkly.sdk.server.DataModel.SEGMENTS;
2026
import static org.junit.Assert.assertEquals;
@@ -27,35 +33,38 @@
2733
public class DataModelSerializationTest {
2834
@Test
2935
public void flagIsDeserializedWithAllProperties() {
30-
String json0 = flagWithAllPropertiesJson().toJsonString();
31-
FeatureFlag flag0 = (FeatureFlag)FEATURES.deserialize(json0).getItem();
32-
assertFlagHasAllProperties(flag0);
33-
34-
String json1 = FEATURES.serialize(new ItemDescriptor(flag0.getVersion(), flag0));
35-
FeatureFlag flag1 = (FeatureFlag)FEATURES.deserialize(json1).getItem();
36-
assertFlagHasAllProperties(flag1);
36+
assertFlagFromJson(
37+
flagWithAllPropertiesJson(),
38+
flag -> {
39+
assertFlagHasAllProperties(flag);
40+
41+
String json1 = FEATURES.serialize(new ItemDescriptor(flag.getVersion(), flag));
42+
assertFlagFromJson(LDValue.parse(json1), flag1 -> assertFlagHasAllProperties(flag1));
43+
});
3744
}
3845

3946
@Test
4047
public void flagIsDeserializedWithMinimalProperties() {
41-
String json = LDValue.buildObject().put("key", "flag-key").put("version", 99).build().toJsonString();
42-
FeatureFlag flag = (FeatureFlag)FEATURES.deserialize(json).getItem();
43-
assertEquals("flag-key", flag.getKey());
44-
assertEquals(99, flag.getVersion());
45-
assertFalse(flag.isOn());
46-
assertNull(flag.getSalt());
47-
assertNotNull(flag.getTargets());
48-
assertEquals(0, flag.getTargets().size());
49-
assertNotNull(flag.getRules());
50-
assertEquals(0, flag.getRules().size());
51-
assertNull(flag.getFallthrough());
52-
assertNull(flag.getOffVariation());
53-
assertNotNull(flag.getVariations());
54-
assertEquals(0, flag.getVariations().size());
55-
assertFalse(flag.isClientSide());
56-
assertFalse(flag.isTrackEvents());
57-
assertFalse(flag.isTrackEventsFallthrough());
58-
assertNull(flag.getDebugEventsUntilDate());
48+
assertFlagFromJson(
49+
LDValue.buildObject().put("key", "flag-key").put("version", 99).build(),
50+
flag -> {
51+
assertEquals("flag-key", flag.getKey());
52+
assertEquals(99, flag.getVersion());
53+
assertFalse(flag.isOn());
54+
assertNull(flag.getSalt());
55+
assertNotNull(flag.getTargets());
56+
assertEquals(0, flag.getTargets().size());
57+
assertNotNull(flag.getRules());
58+
assertEquals(0, flag.getRules().size());
59+
assertNull(flag.getFallthrough());
60+
assertNull(flag.getOffVariation());
61+
assertNotNull(flag.getVariations());
62+
assertEquals(0, flag.getVariations().size());
63+
assertFalse(flag.isClientSide());
64+
assertFalse(flag.isTrackEvents());
65+
assertFalse(flag.isTrackEventsFallthrough());
66+
assertNull(flag.getDebugEventsUntilDate());
67+
});
5968
}
6069

6170
@Test
@@ -109,6 +118,147 @@ public void deletedSegmentIsConvertedToAndFromJsonPlaceholder() {
109118
assertEquals(LDValue.parse(json0), LDValue.parse(json1));
110119
}
111120

121+
@Test
122+
public void explicitNullsAreToleratedForNullableValues() {
123+
// Nulls are not *always* valid-- it is OK to raise a deserialization error if a null appears
124+
// where a non-nullable primitive type like boolean is expected, so for instance "version":null
125+
// is invalid. But for anything that is optional, an explicit null is equivalent to omitting
126+
// the property. Note: it would be nice to use Optional<T> for things like this, but we can't
127+
// do it because Gson does not play well with Optional.
128+
assertFlagFromJson(
129+
baseBuilder("flag-key").put("offVariation", LDValue.ofNull()).build(),
130+
flag -> assertNull(flag.getOffVariation())
131+
);
132+
assertFlagFromJson(
133+
baseBuilder("flag-key")
134+
.put("fallthrough", LDValue.buildObject().put("rollout", LDValue.ofNull()).build())
135+
.build(),
136+
flag -> assertNull(flag.getFallthrough().getRollout())
137+
);
138+
assertFlagFromJson(
139+
baseBuilder("flag-key")
140+
.put("fallthrough", LDValue.buildObject().put("variation", LDValue.ofNull()).build())
141+
.build(),
142+
flag -> assertNull(flag.getFallthrough().getVariation())
143+
);
144+
145+
// Nulls for list values should always be considered equivalent to an empty list, because
146+
// that's how Go would serialize a nil slice
147+
assertFlagFromJson(
148+
baseBuilder("flag-key").put("prerequisites", LDValue.ofNull()).build(),
149+
flag -> assertEquals(Collections.<Prerequisite>emptyList(), flag.getPrerequisites())
150+
);
151+
assertFlagFromJson(
152+
baseBuilder("flag-key").put("rules", LDValue.ofNull()).build(),
153+
flag -> assertEquals(Collections.<Rule>emptyList(), flag.getRules())
154+
);
155+
assertFlagFromJson(
156+
baseBuilder("flag-key").put("targets", LDValue.ofNull()).build(),
157+
flag -> assertEquals(Collections.<Target>emptyList(), flag.getTargets())
158+
);
159+
assertFlagFromJson(
160+
baseBuilder("flag-key")
161+
.put("rules", LDValue.arrayOf(
162+
LDValue.buildObject().put("clauses", LDValue.ofNull()).build()
163+
))
164+
.build(),
165+
flag -> assertEquals(Collections.<Clause>emptyList(), flag.getRules().get(0).getClauses())
166+
);
167+
assertFlagFromJson(
168+
baseBuilder("flag-key")
169+
.put("rules", LDValue.arrayOf(
170+
LDValue.buildObject().put("clauses", LDValue.arrayOf(
171+
LDValue.buildObject().put("values", LDValue.ofNull()).build()
172+
)).build()
173+
))
174+
.build(),
175+
flag -> assertEquals(Collections.<LDValue>emptyList(),
176+
flag.getRules().get(0).getClauses().get(0).getValues())
177+
);
178+
assertFlagFromJson(
179+
baseBuilder("flag-key")
180+
.put("targets", LDValue.arrayOf(
181+
LDValue.buildObject().put("values", LDValue.ofNull()).build()
182+
))
183+
.build(),
184+
flag -> assertEquals(Collections.<String>emptySet(), flag.getTargets().get(0).getValues())
185+
);
186+
assertFlagFromJson(
187+
baseBuilder("flag-key")
188+
.put("fallthrough", LDValue.buildObject().put("rollout",
189+
LDValue.buildObject().put("variations", LDValue.ofNull()).build()
190+
).build())
191+
.build(),
192+
flag -> assertEquals(Collections.<WeightedVariation>emptyList(),
193+
flag.getFallthrough().getRollout().getVariations())
194+
);
195+
assertSegmentFromJson(
196+
baseBuilder("segment-key").put("rules", LDValue.ofNull()).build(),
197+
segment -> assertEquals(Collections.<SegmentRule>emptyList(), segment.getRules())
198+
);
199+
assertSegmentFromJson(
200+
baseBuilder("segment-key")
201+
.put("rules", LDValue.arrayOf(
202+
LDValue.buildObject().put("clauses", LDValue.ofNull()).build()
203+
))
204+
.build(),
205+
segment -> assertEquals(Collections.<Clause>emptyList(), segment.getRules().get(0).getClauses())
206+
);
207+
208+
// Nulls in clause values are not useful since the clause can never match, but they're valid JSON;
209+
// we should normalize them to LDValue.ofNull() to avoid potential NPEs down the line
210+
assertFlagFromJson(
211+
baseBuilder("flag-key")
212+
.put("rules", LDValue.arrayOf(
213+
LDValue.buildObject()
214+
.put("clauses", LDValue.arrayOf(
215+
LDValue.buildObject()
216+
.put("values", LDValue.arrayOf(LDValue.ofNull()))
217+
.build()
218+
))
219+
.build()
220+
))
221+
.build(),
222+
flag -> assertEquals(LDValue.ofNull(),
223+
flag.getRules().get(0).getClauses().get(0).getValues().get(0))
224+
);
225+
assertSegmentFromJson(
226+
baseBuilder("segment-key")
227+
.put("rules", LDValue.arrayOf(
228+
LDValue.buildObject()
229+
.put("clauses", LDValue.arrayOf(
230+
LDValue.buildObject()
231+
.put("values", LDValue.arrayOf(LDValue.ofNull()))
232+
.build()
233+
))
234+
.build()
235+
))
236+
.build(),
237+
segment -> assertEquals(LDValue.ofNull(),
238+
segment.getRules().get(0).getClauses().get(0).getValues().get(0))
239+
);
240+
241+
// Similarly, null for a flag variation isn't a useful value but it is valid JSON
242+
assertFlagFromJson(
243+
baseBuilder("flagKey").put("variations", LDValue.arrayOf(LDValue.ofNull())).build(),
244+
flag -> assertEquals(LDValue.ofNull(), flag.getVariations().get(0))
245+
);
246+
}
247+
248+
private void assertFlagFromJson(LDValue flagJson, Consumer<FeatureFlag> action) {
249+
FeatureFlag flag = (FeatureFlag)FEATURES.deserialize(flagJson.toJsonString()).getItem();
250+
action.accept(flag);
251+
}
252+
253+
private void assertSegmentFromJson(LDValue segmentJson, Consumer<Segment> action) {
254+
Segment segment = (Segment)SEGMENTS.deserialize(segmentJson.toJsonString()).getItem();
255+
action.accept(segment);
256+
}
257+
258+
private ObjectBuilder baseBuilder(String key) {
259+
return LDValue.buildObject().put("key", key).put("version", 99);
260+
}
261+
112262
private LDValue flagWithAllPropertiesJson() {
113263
return LDValue.buildObject()
114264
.put("key", "flag-key")

0 commit comments

Comments
 (0)