-
Notifications
You must be signed in to change notification settings - Fork 0
feat: implement v4 client-side evaluation #51
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
lourens-octopus
wants to merge
8
commits into
main
Choose a base branch
from
lourens/bmbb-751
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,478
−273
Open
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
611d2ed
feat: implement v4 client-side evaluation
lourens-octopus c5acc42
test: cover every unusable condition discriminator shape
lourens-octopus 9029590
test: drop the JSON property capitalisation tests
lourens-octopus 89fbe02
feat: require a reason on a server-resolved flag
lourens-octopus 1a6e2a5
refactor: align the rollout helper with the other provider libraries
lourens-octopus 1263ff1
docs: refer to the Feature Flags service rather than OctoToggle
lourens-octopus d579e29
fix: do not require a slug at parse time
lourens-octopus 1831d58
refactor: keep the rollout helper out of the public API
lourens-octopus File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
29 changes: 10 additions & 19 deletions
29
src/main/java/com/octopus/openfeature/provider/v4/ClientSideCondition.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,33 +1,24 @@ | ||
| package com.octopus.openfeature.provider.v4; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonSubTypes; | ||
| import com.fasterxml.jackson.annotation.JsonTypeInfo; | ||
| import com.fasterxml.jackson.databind.annotation.JsonDeserialize; | ||
|
|
||
| /** | ||
| * Base type for a client-side rule condition, selected from the camelCase {@code type} discriminator | ||
| * when deserializing a v4 evaluation response. These types model the wire shape only. | ||
| * | ||
| * <p>A discriminator this version of the provider does not recognise — or an absent one — | ||
| * deserializes to {@link UnknownCondition} rather than failing, so a condition type | ||
| * introduced by a newer server degrades safely on an older client. | ||
| * when deserializing a v4 evaluation response. | ||
| * | ||
| * <p>The conditions sit alongside the rest of the v4 types rather than in a {@code conditions} | ||
| * sub-package as the .NET provider has them. Java package access is not hierarchical, so a | ||
| * package-private condition there would be invisible to {@link ClientSideRule} in this package, and | ||
| * following that layout would mean making every condition public — part of the library's supported | ||
| * API, which is what keeping these types package-private is meant to avoid. | ||
| */ | ||
| @JsonTypeInfo( | ||
| use = JsonTypeInfo.Id.NAME, | ||
| include = JsonTypeInfo.As.PROPERTY, | ||
| property = "type", | ||
| visible = true, | ||
| defaultImpl = UnknownCondition.class | ||
| ) | ||
| @JsonSubTypes({ | ||
| @JsonSubTypes.Type(value = PercentageByContextCondition.class, name = ConditionTypeNames.PERCENTAGE_BY_CONTEXT), | ||
| @JsonSubTypes.Type(value = ContextAttributeIsOneOfCondition.class, name = ConditionTypeNames.CONTEXT_ATTRIBUTE_IS_ONE_OF), | ||
| @JsonSubTypes.Type(value = ContextAttributeIsNotOneOfCondition.class, name = ConditionTypeNames.CONTEXT_ATTRIBUTE_IS_NOT_ONE_OF) | ||
| }) | ||
| @JsonDeserialize(using = ClientSideConditionDeserializer.class) | ||
| abstract class ClientSideCondition { | ||
|
|
||
| /** | ||
| * Whether this condition is met. A condition that did not arrive in a shape its type can evaluate | ||
| * throws {@link dev.openfeature.sdk.exceptions.ParseError} rather than reading a value it was not | ||
| * sent. | ||
| */ | ||
| abstract boolean matches(ClientSideEvaluationContext context); | ||
| } |
53 changes: 53 additions & 0 deletions
53
src/main/java/com/octopus/openfeature/provider/v4/ClientSideConditionDeserializer.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| package com.octopus.openfeature.provider.v4; | ||
|
|
||
| import com.fasterxml.jackson.core.JsonParser; | ||
| import com.fasterxml.jackson.core.ObjectCodec; | ||
| import com.fasterxml.jackson.databind.DeserializationContext; | ||
| import com.fasterxml.jackson.databind.JsonDeserializer; | ||
| import com.fasterxml.jackson.databind.JsonNode; | ||
|
|
||
| import java.io.IOException; | ||
|
|
||
| /** | ||
| * Selects the concrete {@link ClientSideCondition} from the camelCase {@code type} discriminator. An | ||
| * unrecognised discriminator deserializes to {@link UnknownCondition} rather than throwing, so a | ||
| * condition type introduced by a newer server degrades safely on an older client. | ||
| * | ||
| * <p>Written by hand rather than driven by {@code @JsonTypeInfo}, which cannot distinguish a | ||
| * discriminator that is not a string — Jackson coerces {@code "type": 123} to {@code "123"} and | ||
| * treats it as merely unrecognised. Here a non-string (or absent) discriminator yields an | ||
| * {@link UnknownCondition} carrying no type, which fails evaluation as the malformed response it is. | ||
| * | ||
| * <p>The provider only ever reads these conditions, so serialization is left to Jackson's defaults. | ||
| */ | ||
| final class ClientSideConditionDeserializer extends JsonDeserializer<ClientSideCondition> { | ||
|
|
||
| private static final String DISCRIMINATOR = "type"; | ||
|
|
||
| @Override | ||
| public ClientSideCondition deserialize(JsonParser parser, DeserializationContext context) throws IOException { | ||
| ObjectCodec codec = parser.getCodec(); | ||
| JsonNode node = codec.readTree(parser); | ||
|
|
||
| // Matched exactly, as the .NET provider's converter does: the server always sends "type". | ||
| JsonNode discriminator = node.get(DISCRIMINATOR); | ||
| String type = discriminator != null && discriminator.isTextual() ? discriminator.textValue() : null; | ||
|
|
||
| // Deserializing the concrete type targets that type directly, so this deserializer — registered | ||
| // on the base type only — is not re-entered. | ||
| if (type == null) { | ||
| return new UnknownCondition(null); | ||
| } | ||
|
|
||
| switch (type) { | ||
| case ConditionTypeNames.PERCENTAGE_BY_CONTEXT: | ||
| return codec.treeToValue(node, PercentageByContextCondition.class); | ||
| case ConditionTypeNames.CONTEXT_ATTRIBUTE_IS_ONE_OF: | ||
| return codec.treeToValue(node, ContextAttributeIsOneOfCondition.class); | ||
| case ConditionTypeNames.CONTEXT_ATTRIBUTE_IS_NOT_ONE_OF: | ||
| return codec.treeToValue(node, ContextAttributeIsNotOneOfCondition.class); | ||
| default: | ||
| return new UnknownCondition(type); | ||
| } | ||
| } | ||
| } |
30 changes: 30 additions & 0 deletions
30
src/main/java/com/octopus/openfeature/provider/v4/ClientSideEvaluationContext.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| package com.octopus.openfeature.provider.v4; | ||
|
|
||
| import dev.openfeature.sdk.EvaluationContext; | ||
|
|
||
| /** | ||
| * What a flag's rules and conditions are evaluated against. | ||
| */ | ||
| final class ClientSideEvaluationContext { | ||
| private final String evaluationKey; | ||
| private final EvaluationContext openFeatureContext; | ||
|
|
||
| ClientSideEvaluationContext(String evaluationKey, EvaluationContext openFeatureContext) { | ||
| this.evaluationKey = evaluationKey; | ||
| this.openFeatureContext = openFeatureContext; | ||
| } | ||
|
|
||
| /** | ||
| * The key {@code percentage-by-context} buckets against. | ||
| */ | ||
| String getEvaluationKey() { | ||
| return evaluationKey; | ||
| } | ||
|
|
||
| /** | ||
| * The caller's context, or {@code null} if they supplied none. | ||
| */ | ||
| EvaluationContext getOpenFeatureContext() { | ||
| return openFeatureContext; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
51 changes: 51 additions & 0 deletions
51
src/main/java/com/octopus/openfeature/provider/v4/ContextAttributes.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| package com.octopus.openfeature.provider.v4; | ||
|
|
||
| import dev.openfeature.sdk.EvaluationContext; | ||
| import dev.openfeature.sdk.exceptions.ParseError; | ||
|
|
||
| import java.util.List; | ||
| import java.util.Objects; | ||
|
|
||
| /** | ||
| * The attribute lookup shared by both attribute conditions. | ||
| */ | ||
| final class ContextAttributes { | ||
|
|
||
| private ContextAttributes() { | ||
| } | ||
|
|
||
| /** | ||
| * Whether the context holds an attribute named {@code key} with one of {@code values}. | ||
| * | ||
| * <p>Mirrors v3 segment matching: keys and values compare case-insensitively and a non-string | ||
| * value counts as absent. Every entry whose key matches is checked, not just the first — a context | ||
| * can hold several case variants of one key. | ||
| */ | ||
| static boolean isOneOf(ClientSideEvaluationContext context, String key, List<String> values) { | ||
| if (key == null) { | ||
| throw new ParseError("A condition is missing a key."); | ||
| } | ||
|
|
||
| if (values == null || values.isEmpty()) { | ||
| throw new ParseError("A condition is missing values."); | ||
| } | ||
|
|
||
| if (values.stream().anyMatch(Objects::isNull)) { | ||
| throw new ParseError("A condition is missing a value."); | ||
| } | ||
|
|
||
| EvaluationContext openFeatureContext = context.getOpenFeatureContext(); | ||
| if (openFeatureContext == null) { | ||
| return false; | ||
| } | ||
|
|
||
| return openFeatureContext.asMap().entrySet().stream().anyMatch(entry -> { | ||
| if (!entry.getKey().equalsIgnoreCase(key)) { | ||
| return false; | ||
| } | ||
|
|
||
| String attribute = entry.getValue().asString(); | ||
| return attribute != null && values.stream().anyMatch(value -> value.equalsIgnoreCase(attribute)); | ||
| }); | ||
| } | ||
| } |
19 changes: 19 additions & 0 deletions
19
src/main/java/com/octopus/openfeature/provider/v4/EvaluationReasons.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| package com.octopus.openfeature.provider.v4; | ||
|
|
||
| /** | ||
| * Reasons returned alongside a client-side evaluation. Both match the strings the Feature Flags service | ||
| * produces server-side, so a flag reads the same whichever side resolved it. | ||
| */ | ||
| final class EvaluationReasons { | ||
|
|
||
| private EvaluationReasons() { | ||
| } | ||
|
|
||
| static String matchedRule(String ruleName) { | ||
| return "Matched rule '" + ruleName + "'."; | ||
| } | ||
|
|
||
| static String didNotMatchAnyRules() { | ||
| return "Did not match any rules."; | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ListUtils?