Skip to content

Commit 7b4839f

Browse files
authored
Use new evaluations endpoint (#7)
1 parent 51d5af0 commit 7b4839f

18 files changed

Lines changed: 183 additions & 106 deletions

src/main/java/com/octopus/openfeature/provider/FeatureToggleEvaluation.java

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,30 @@
33
import com.fasterxml.jackson.annotation.JsonCreator;
44
import com.fasterxml.jackson.annotation.JsonProperty;
55

6-
import java.util.Collections;
7-
import java.util.ArrayList;
86
import java.util.List;
7+
import java.util.Optional;
98

109
class FeatureToggleEvaluation {
11-
private final String name;
1210
private final String slug;
1311
private final boolean isEnabled;
12+
private final String evaluationKey;
1413
private final List<Segment> segments;
14+
private final Integer clientRolloutPercentage;
1515

1616
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
1717
FeatureToggleEvaluation(
18-
@JsonProperty("name") String name,
19-
@JsonProperty("slug") String slug,
20-
@JsonProperty("isEnabled") boolean isEnabled,
21-
@JsonProperty("segments") List<Segment> segments
18+
@JsonProperty(value = "slug", required = true) String slug,
19+
@JsonProperty(value = "isEnabled", required = true) boolean isEnabled,
20+
@JsonProperty("evaluationKey") String evaluationKey,
21+
@JsonProperty("segments") List<Segment> segments,
22+
@JsonProperty("clientRolloutPercentage") Integer clientRolloutPercentage
2223
) {
23-
this.name = name;
2424
this.slug = slug;
2525
this.isEnabled = isEnabled;
2626

27-
this.segments = new ArrayList<>();
28-
if (segments != null) {
29-
this.segments.addAll(segments);
30-
}
31-
}
32-
33-
public String getName() {
34-
return name;
27+
this.evaluationKey = evaluationKey;
28+
this.segments = segments == null ? null : List.copyOf(segments);
29+
this.clientRolloutPercentage = clientRolloutPercentage;
3530
}
3631

3732
public String getSlug() {
@@ -42,7 +37,19 @@ public boolean isEnabled() {
4237
return isEnabled;
4338
}
4439

45-
public List<Segment> getSegments() {
46-
return Collections.unmodifiableList(segments);
40+
public Optional<String> getEvaluationKey() {
41+
return Optional.ofNullable(evaluationKey);
42+
}
43+
44+
public Optional<List<Segment>> getSegments() {
45+
return Optional.ofNullable(segments);
46+
}
47+
48+
public boolean hasSegments() {
49+
return segments != null && !segments.isEmpty();
50+
}
51+
52+
public Optional<Integer> getClientRolloutPercentage() {
53+
return Optional.ofNullable(clientRolloutPercentage);
4754
}
4855
}

src/main/java/com/octopus/openfeature/provider/OctopusClient.java

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.octopus.openfeature.provider;
22

33
import com.fasterxml.jackson.core.type.TypeReference;
4-
import com.fasterxml.jackson.databind.ObjectMapper;
54
import java.net.MalformedURLException;
65
import java.net.URI;
76
import java.net.URISyntaxException;
@@ -65,7 +64,7 @@ FeatureToggles getFeatureToggleEvaluationManifest()
6564
logger.log(System.Logger.Level.WARNING,String.format("Feature toggle response from %s did not contain expected ContentHash header", manifestURI.toString()));
6665
return null;
6766
}
68-
List<FeatureToggleEvaluation> evaluations = OctopusObjectMapper.INSTANCE.readValue(httpResponse.body(), new TypeReference<List<FeatureToggleEvaluation>>(){});
67+
List<FeatureToggleEvaluation> evaluations = OctopusObjectMapper.INSTANCE.readValue(httpResponse.body(), new TypeReference<>(){});
6968
return new FeatureToggles(evaluations, Base64.getDecoder().decode(contentHashHeader.get()));
7069
} catch (Exception e) {
7170
logger.log(System.Logger.Level.WARNING, "Unable to query Octopus Feature Toggle service", e);
@@ -83,16 +82,12 @@ private URI getCheckURI() {
8382

8483
private URI getManifestURI() {
8584
try {
86-
return new URL(config.getServerUri().toURL(), "/api/featuretoggles/v3/").toURI();
85+
return new URL(config.getServerUri().toURL(), "/api/toggles/evaluations/v3/").toURI();
8786
} catch (MalformedURLException | URISyntaxException ignored) // we know this URL is well-formed
8887
{ }
8988
return null;
9089
}
9190

92-
private Boolean isSuccessStatusCode(int statusCode) {
93-
return statusCode >= 200 && statusCode < 300;
94-
}
95-
9691
// This class needs to be static to allow deserialization
9792
private static class FeatureToggleCheckResponse {
9893
public byte[] contentHash;

src/main/java/com/octopus/openfeature/provider/OctopusContext.java

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
import dev.openfeature.sdk.*;
44
import dev.openfeature.sdk.exceptions.FlagNotFoundError;
5+
import dev.openfeature.sdk.exceptions.ParseError;
56

67
import java.util.List;
7-
import java.util.Map;
88

99
import static java.util.stream.Collectors.groupingBy;
1010

@@ -20,35 +20,51 @@ class OctopusContext {
2020
static OctopusContext empty() {
2121
return new OctopusContext(new FeatureToggles(List.of(), new byte[0]));
2222
}
23-
24-
byte[] getContentHash() { return featureToggles.getContentHash(); }
23+
24+
byte[] getContentHash() {
25+
return featureToggles.getContentHash();
26+
}
2527

2628
ProviderEvaluation<Boolean> evaluate(String slug, Boolean defaultValue, EvaluationContext evaluationContext) {
2729
// find the feature toggle matching the slug
2830
var toggleValue = featureToggles.getEvaluations().stream().filter(f -> f.getSlug().equalsIgnoreCase(slug)).findFirst().orElse(null);
2931

3032
// this exception will be handled by OpenFeature, and the default value will be used
3133
if (toggleValue == null) {
32-
throw new FlagNotFoundError();
34+
throw new FlagNotFoundError();
35+
}
36+
37+
if (missingRequiredPropertiesForClientSideEvaluation(toggleValue)) {
38+
throw new ParseError("Feature toggle " + toggleValue.getSlug() + " is missing necessary information for client-side evaluation.");
3339
}
34-
35-
// if the toggle is disabled, or if it has no segments, then we don't need to evaluate dynamically
36-
if (!toggleValue.isEnabled() || toggleValue.getSegments().isEmpty()) {
40+
41+
// if the toggle is disabled, or if it has no segments, then we don't need to evaluate dynamically
42+
if (!toggleValue.isEnabled() || !toggleValue.hasSegments()) {
3743
return ProviderEvaluation.<Boolean>builder()
3844
.value(toggleValue.isEnabled())
3945
.reason(Reason.DEFAULT.toString())
4046
.build();
4147
}
42-
43-
// If the toggle is enabled and has segments configured, then we need to evaluate dynamically,
48+
49+
// If the toggle is enabled and has segments configured, then we need to evaluate dynamically,
4450
// checking the context matches the segments
4551
return ProviderEvaluation.<Boolean>builder()
46-
.value(MatchesSegment(evaluationContext, toggleValue.getSegments()))
52+
.value(matchesSegment(evaluationContext, toggleValue.getSegments().orElseThrow())) // checked in hasSegments
4753
.reason(Reason.TARGETING_MATCH.toString())
4854
.build();
4955
}
5056

51-
private Boolean MatchesSegment(EvaluationContext evaluationContext, List<Segment> segments) {
57+
private boolean missingRequiredPropertiesForClientSideEvaluation(FeatureToggleEvaluation evaluation) {
58+
if (!evaluation.isEnabled()) {
59+
return false;
60+
}
61+
62+
return evaluation.getClientRolloutPercentage().isEmpty()
63+
|| evaluation.getEvaluationKey().isEmpty()
64+
|| evaluation.getSegments().isEmpty();
65+
}
66+
67+
private Boolean matchesSegment(EvaluationContext evaluationContext, List<Segment> segments) {
5268
if (evaluationContext == null) {
5369
return false;
5470
}

src/test/java/com/octopus/openfeature/provider/FeatureToggleEvaluationDeserializationTests.java

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import java.io.InputStream;
99
import java.util.List;
10-
import java.util.Map;
10+
import java.util.Optional;
1111

1212
import static org.assertj.core.api.Assertions.assertThat;
1313
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -28,33 +28,41 @@ private void assertSegmentsContain(List<Segment> segments, Segment... expected)
2828
void shouldDeserializeEnabledToggle() throws Exception {
2929
FeatureToggleEvaluation result = objectMapper.readValue(resource("toggle-enabled-no-segments.json"), FeatureToggleEvaluation.class);
3030

31-
assertThat(result.getName()).isEqualTo("My Feature");
3231
assertThat(result.getSlug()).isEqualTo("my-feature");
3332
assertThat(result.isEnabled()).isTrue();
34-
assertThat(result.getSegments()).isEmpty();
33+
assertThat(result.getEvaluationKey()).hasValue("eval-key-1");
34+
assertThat(result.getSegments()).isPresent();
35+
assertThat(result.getSegments().orElseThrow()).isEmpty();
36+
assertThat(result.getClientRolloutPercentage()).hasValue(100);
3537
}
3638

3739
@Test
3840
void shouldDeserializeDisabledToggle() throws Exception {
3941
FeatureToggleEvaluation result = objectMapper.readValue(resource("toggle-disabled.json"), FeatureToggleEvaluation.class);
4042

4143
assertThat(result.isEnabled()).isFalse();
44+
assertThat(result.getEvaluationKey()).isEmpty();
45+
assertThat(result.getSegments()).isEmpty();
46+
assertThat(result.getClientRolloutPercentage()).isEmpty();
4247
}
4348

4449
@Test
4550
void shouldDeserializeToggleWithMissingSegmentsField() throws Exception {
4651
FeatureToggleEvaluation result = objectMapper.readValue(resource("toggle-missing-segments.json"), FeatureToggleEvaluation.class);
4752

48-
assertThat(result.getSegments()).isNotNull().isEmpty();
53+
assertThat(result.getSegments()).isEmpty();
4954
}
5055

5156
@Test
5257
void shouldDeserializeToggleWithSegments() throws Exception {
5358
FeatureToggleEvaluation result = objectMapper.readValue(
5459
resource("toggle-with-segments.json"), FeatureToggleEvaluation.class);
5560

56-
assertThat(result.getSegments()).hasSize(2);
57-
assertSegmentsContain(result.getSegments(),
61+
var segments = result.getSegments().orElseThrow();
62+
63+
assertThat(segments).hasSize(2);
64+
assertSegmentsContain(
65+
segments,
5866
new Segment("license-type", "free"),
5967
new Segment("country", "au")
6068
);
@@ -86,13 +94,13 @@ void shouldDeserializeListOfTogglesWithVariousFieldCasings() throws Exception {
8694
assertThat(result).hasSize(3);
8795
assertThat(result.get(0).getSlug()).isEqualTo("feature-a");
8896
assertThat(result.get(0).isEnabled()).isTrue();
89-
assertSegmentsContain(result.get(0).getSegments(), new Segment("license-type", "free"));
97+
assertSegmentsContain(result.get(0).getSegments().orElseThrow(), new Segment("license-type", "free"));
9098
assertThat(result.get(1).getSlug()).isEqualTo("feature-b");
9199
assertThat(result.get(1).isEnabled()).isTrue();
92-
assertSegmentsContain(result.get(1).getSegments(), new Segment("plan", "enterprise"));
100+
assertSegmentsContain(result.get(1).getSegments().orElseThrow(), new Segment("plan", "enterprise"));
93101
assertThat(result.get(2).getSlug()).isEqualTo("feature-c");
94102
assertThat(result.get(2).isEnabled()).isTrue();
95-
assertSegmentsContain(result.get(2).getSegments(), new Segment("country", "au"));
103+
assertSegmentsContain(result.get(2).getSegments().orElseThrow(), new Segment("country", "au"));
96104
}
97105

98106
@Test
@@ -121,9 +129,8 @@ void shouldIgnoreExtraneousProperties() throws Exception {
121129
FeatureToggleEvaluation result = objectMapper.readValue(
122130
resource("toggle-with-extraneous-properties.json"), FeatureToggleEvaluation.class);
123131

124-
assertThat(result.getName()).isEqualTo("My Feature");
125132
assertThat(result.getSlug()).isEqualTo("my-feature");
126133
assertThat(result.isEnabled()).isTrue();
127-
assertSegmentsContain(result.getSegments(), new Segment("license-type", "free"));
134+
assertSegmentsContain(result.getSegments().orElseThrow(), new Segment("license-type", "free"));
128135
}
129136
}

0 commit comments

Comments
 (0)