-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add v4 evaluation response types to provider library #52
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| package com.octopus.openfeature.provider.v4; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonSubTypes; | ||
| import com.fasterxml.jackson.annotation.JsonTypeInfo; | ||
|
|
||
| /** | ||
| * 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. | ||
| * | ||
| * <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) | ||
| }) | ||
| abstract class ClientSideCondition { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| package com.octopus.openfeature.provider.v4; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonCreator; | ||
| import com.fasterxml.jackson.annotation.JsonProperty; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| /** | ||
| * A named rule the provider library still has to evaluate on the client side. The rule matches when | ||
| * every one of its conditions matches. | ||
| */ | ||
| final class ClientSideRule { | ||
| private final String name; | ||
| private final List<ClientSideCondition> conditions; | ||
|
|
||
| @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) | ||
| ClientSideRule( | ||
| @JsonProperty(value = "name", required = true) String name, | ||
| @JsonProperty(value = "conditions", required = true) List<ClientSideCondition> conditions | ||
| ) { | ||
| this.name = name; | ||
| this.conditions = List.copyOf(conditions); | ||
| } | ||
|
|
||
| public String getName() { | ||
| return name; | ||
| } | ||
|
|
||
| public List<ClientSideCondition> getConditions() { | ||
| return conditions; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package com.octopus.openfeature.provider.v4; | ||
|
|
||
| /** | ||
| * Discriminator values for the polymorphic v4 client-side conditions. These mirror the values in the | ||
| * evaluation response, so they must not drift from the server. | ||
| */ | ||
| final class ConditionTypeNames { | ||
| static final String CONTEXT_ATTRIBUTE_IS_NOT_ONE_OF = "context-attribute-is-not-one-of"; | ||
| static final String CONTEXT_ATTRIBUTE_IS_ONE_OF = "context-attribute-is-one-of"; | ||
| static final String PERCENTAGE_BY_CONTEXT = "percentage-by-context"; | ||
|
|
||
| private ConditionTypeNames() { | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| package com.octopus.openfeature.provider.v4; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonCreator; | ||
| import com.fasterxml.jackson.annotation.JsonIgnoreProperties; | ||
| import com.fasterxml.jackson.annotation.JsonProperty; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| /** | ||
| * Matches when the context attribute {@code key} is not one of {@code values}. | ||
| */ | ||
| @JsonIgnoreProperties("type") // The discriminator is visible to subtypes; this type does not model it. | ||
| final class ContextAttributeIsNotOneOfCondition extends ClientSideCondition { | ||
| private final String key; | ||
| private final List<String> values; | ||
|
|
||
| @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) | ||
| ContextAttributeIsNotOneOfCondition( | ||
| @JsonProperty(value = "key", required = true) String key, | ||
| @JsonProperty(value = "values", required = true) List<String> values | ||
| ) { | ||
| this.key = key; | ||
| this.values = List.copyOf(values); | ||
| } | ||
|
|
||
| public String getKey() { | ||
| return key; | ||
| } | ||
|
|
||
| public List<String> getValues() { | ||
| return values; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| package com.octopus.openfeature.provider.v4; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonCreator; | ||
| import com.fasterxml.jackson.annotation.JsonIgnoreProperties; | ||
| import com.fasterxml.jackson.annotation.JsonProperty; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| /** | ||
| * Matches when the context attribute {@code key} is one of {@code values}. | ||
| */ | ||
| @JsonIgnoreProperties("type") // The discriminator is visible to subtypes; this type does not model it. | ||
| final class ContextAttributeIsOneOfCondition extends ClientSideCondition { | ||
| private final String key; | ||
| private final List<String> values; | ||
|
|
||
| @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) | ||
| ContextAttributeIsOneOfCondition( | ||
| @JsonProperty(value = "key", required = true) String key, | ||
| @JsonProperty(value = "values", required = true) List<String> values | ||
| ) { | ||
| this.key = key; | ||
| this.values = List.copyOf(values); | ||
| } | ||
|
|
||
| public String getKey() { | ||
| return key; | ||
| } | ||
|
|
||
| public List<String> getValues() { | ||
| return values; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| package com.octopus.openfeature.provider.v4; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonCreator; | ||
| import com.fasterxml.jackson.annotation.JsonIgnoreProperties; | ||
| import com.fasterxml.jackson.annotation.JsonProperty; | ||
|
|
||
| /** | ||
| * Matches when the OpenFeature targeting key falls within the {@code percentage}% rollout. | ||
| */ | ||
| @JsonIgnoreProperties("type") // The discriminator is visible to subtypes; this type does not model it. | ||
| final class PercentageByContextCondition extends ClientSideCondition { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The C# equivalent for this condition has a nullable By the way, happy to defer that conversation to one of the following PRs. I think the specification tests should catch this if needed.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. haha yeah, next PR changes it. This looks like a follow on from my prompt. I gave it the equivalent dotnet PR and it did the same in that one, so both sets of PRs match in that respect now |
||
| private final int percentage; | ||
|
|
||
| @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) | ||
| PercentageByContextCondition( | ||
| @JsonProperty(value = "percentage", required = true) int percentage | ||
| ) { | ||
| this.percentage = percentage; | ||
| } | ||
|
|
||
| public int getPercentage() { | ||
| return percentage; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| package com.octopus.openfeature.provider.v4; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonCreator; | ||
| import com.fasterxml.jackson.annotation.JsonProperty; | ||
|
|
||
| import java.util.List; | ||
| import java.util.Optional; | ||
|
|
||
| /** | ||
| * A single feature flag as returned by the OctoToggle v4 evaluations endpoint. The endpoint returns | ||
| * an array of these. | ||
| * | ||
| * <p>A flag is returned in one of two shapes: | ||
| * <ul> | ||
| * <li>Resolved by the server — {@code value} and {@code reason} are populated.</li> | ||
| * <li>Deferred to the client — {@code evaluationKey} and {@code rules} are populated and the | ||
| * provider library must evaluate the remaining client-side conditions.</li> | ||
| * </ul> | ||
| * Properties that do not apply to the returned shape are omitted from the JSON. | ||
| */ | ||
| final class ServerSideEvaluation { | ||
| private final String slug; | ||
| private final Boolean value; | ||
| private final String reason; | ||
| private final String evaluationKey; | ||
| private final List<ClientSideRule> rules; | ||
|
|
||
| @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) | ||
| ServerSideEvaluation( | ||
| @JsonProperty(value = "slug", required = true) String slug, | ||
| @JsonProperty("value") Boolean value, | ||
| @JsonProperty("reason") String reason, | ||
| @JsonProperty("evaluationKey") String evaluationKey, | ||
| @JsonProperty("rules") List<ClientSideRule> rules | ||
| ) { | ||
| this.slug = slug; | ||
| this.value = value; | ||
| this.reason = reason; | ||
| this.evaluationKey = evaluationKey; | ||
| this.rules = rules == null ? null : List.copyOf(rules); | ||
| } | ||
|
|
||
| public String getSlug() { | ||
| return slug; | ||
| } | ||
|
|
||
| public Optional<Boolean> getValue() { | ||
| return Optional.ofNullable(value); | ||
| } | ||
|
|
||
| public Optional<String> getReason() { | ||
| return Optional.ofNullable(reason); | ||
| } | ||
|
|
||
| public Optional<String> getEvaluationKey() { | ||
| return Optional.ofNullable(evaluationKey); | ||
| } | ||
|
|
||
| public Optional<List<ClientSideRule>> getRules() { | ||
| return Optional.ofNullable(rules); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| package com.octopus.openfeature.provider.v4; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonCreator; | ||
| import com.fasterxml.jackson.annotation.JsonProperty; | ||
|
|
||
| import java.util.Optional; | ||
|
|
||
| /** | ||
| * A client-side condition whose {@code type} discriminator this version of the provider does not | ||
| * recognise, or which carried no discriminator at all. Rather than failing the whole evaluation | ||
| * response, an unrecognised condition is preserved as this type. It always evaluates to | ||
| * {@code false}, so a rule containing an unknown condition can never match — a newer server | ||
| * capability is safely treated as "not met" by an older client. | ||
| * | ||
| * <p>The raw payload of an unknown condition is not retained, only its discriminator. | ||
| */ | ||
| final class UnknownCondition extends ClientSideCondition { | ||
| private final String type; | ||
|
|
||
| @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) | ||
| UnknownCondition( | ||
| @JsonProperty("type") String type | ||
| ) { | ||
| this.type = type; | ||
| } | ||
|
|
||
| /** | ||
| * The unrecognised discriminator value, or empty if none was present. | ||
| */ | ||
| public Optional<String> getType() { | ||
| return Optional.ofNullable(type); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package com.octopus.openfeature.provider; | ||
|
|
||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
|
|
||
| /** | ||
| * Exposes the provider's package-private {@link OctopusObjectMapper} to tests in other packages, so | ||
| * they can exercise deserialization with the same mapper the client uses in production rather than | ||
| * a re-declared copy that could drift from it. | ||
| * | ||
| * <p>Test-only: this lives in the test sources and is never published. | ||
| */ | ||
| public final class TestObjectMapper { | ||
| public static final ObjectMapper INSTANCE = OctopusObjectMapper.INSTANCE; | ||
|
|
||
| private TestObjectMapper() { | ||
| } | ||
| } |
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.
Not sure it's strictly necessary, but might be helpful to follow the same directory structure between the libraries. e.g. src/main/java/com/octopus/openfeature/provider/v4/Conditions/ClientSideCondition.java
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.
The package private setup of java makes this tricky. Doing that looks like it forces us to make it public and we'd rather not now to make things easier to update. Updated javadoc of ClientSideCondition with the explanation