-
Notifications
You must be signed in to change notification settings - Fork 78
feat(codereadiness): add hook for version-based flag validation #1819
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
Draft
marcin11858
wants to merge
1
commit into
open-feature:main
Choose a base branch
from
marcin11858:feature/codereadiness-hook
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.
Draft
Changes from all commits
Commits
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| # Code Readiness Hook | ||
|
|
||
| The `codereadiness` hook allows controlling feature flag evaluation based on the version of the application code. | ||
| It does this by comparing the current application version with a required minimum version specified in the flag's metadata. | ||
| If the comparison fails (i.e., the application version is lower than the required version), the hook returns an error, causing the flag evaluation to resolve to its configured default value. | ||
|
|
||
| ## Installation | ||
|
|
||
| ```xml | ||
| <dependency> | ||
| <groupId>dev.openfeature.contrib.hooks</groupId> | ||
| <artifactId>code-readiness-hook</artifactId> | ||
| <version>0.1.0</version> | ||
| </dependency> | ||
| ``` | ||
|
|
||
| ## Setup | ||
|
|
||
| First, import the OpenFeature SDK and the code readiness hook: | ||
|
|
||
| ```java | ||
| import dev.openfeature.sdk.OpenFeatureAPI; | ||
| import dev.openfeature.contrib.hooks.codereadiness.CodeReadinessHook; | ||
| ``` | ||
|
|
||
| Then, configure the hook with the current version of the application code and register it: | ||
|
|
||
| ```java | ||
| // currentVersion is the current version of the code, which can be retrieved | ||
| // from environment variables, build properties, or configuration files. | ||
| String currentVersion = "1.0.0"; | ||
|
|
||
| CodeReadinessHook codeReadinessHook = CodeReadinessHook.builder(currentVersion).build(); | ||
|
|
||
| // Register the hook globally at the OpenFeature API level | ||
| OpenFeatureAPI.getInstance().addHooks(codeReadinessHook); | ||
| ``` | ||
|
|
||
| ## How It Works | ||
|
|
||
| 1. The hook runs during the **After** phase of flag evaluation. | ||
| 2. It extracts the metadata associated with the evaluated flag. | ||
| 3. It looks for a specific metadata key (by default, `minCodeVersion`). | ||
| 4. If found, it compares the current application version against the required minimum version using the configured comparator (by default, a semver comparison). | ||
| 5. If the current version is **lower** than the required version, it returns an error. This triggers the OpenFeature SDK's fallback mechanism, returning the flag's **default value** to the caller. | ||
|
|
||
| ## Options | ||
|
|
||
| The behavior of the hook can be customized by passing options to the builder: | ||
|
|
||
| ### Strict Validation | ||
|
|
||
| By default, the hook will **not** fail if the `minCodeVersion` metadata or the current application version is missing. To enforce version validation and return an error when these versions are missing, use `strictValidation(true)`. | ||
|
|
||
| ```java | ||
| CodeReadinessHook codeReadinessHook = CodeReadinessHook.builder("1.0.0") | ||
| .strictValidation(true) | ||
| .build(); | ||
| ``` | ||
|
|
||
| ### Custom Metadata Key | ||
|
|
||
| To configure the hook to look for a key other than the default `"minCodeVersion"` in the flag's metadata, use `metadataMinVerKey()`. | ||
|
|
||
| ```java | ||
| CodeReadinessHook codeReadinessHook = CodeReadinessHook.builder("1.0.0") | ||
| .metadataMinVerKey("customMetadataKey") | ||
| .build(); | ||
| ``` | ||
|
|
||
| ### Custom Comparator | ||
|
|
||
| By default, the hook performs a standard semver comparison. If the application uses a different versioning scheme (such as date-based versioning, revision numbers, or custom build numbers), a custom comparison interface implementation can be provided using `comparator()`. | ||
|
|
||
| ```java | ||
| import dev.openfeature.contrib.hooks.codereadiness.VersionComparator; | ||
|
|
||
| VersionComparator customComparator = (current, required) -> { | ||
| // Custom comparison logic: return true if current is ready/sufficient, | ||
| // or false if current is lower than required. | ||
| return current.compareTo(required) >= 0; | ||
| }; | ||
|
|
||
| CodeReadinessHook codeReadinessHook = CodeReadinessHook.builder("2026.06.30") | ||
| .comparator(customComparator) | ||
| .build(); | ||
| ``` |
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,31 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
| xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
| <modelVersion>4.0.0</modelVersion> | ||
| <parent> | ||
| <groupId>dev.openfeature.contrib</groupId> | ||
| <artifactId>parent</artifactId> | ||
| <version>[1.0,2.0)</version> | ||
| <relativePath>../../pom.xml</relativePath> | ||
| </parent> | ||
| <groupId>dev.openfeature.contrib.hooks</groupId> | ||
| <artifactId>code-readiness-hook</artifactId> | ||
| <version>0.1.0</version> <!--x-release-please-version --> | ||
|
|
||
| <name>code-readiness-hook</name> | ||
| <description>Code Readiness Hook</description> | ||
| <url>https://openfeature.dev</url> | ||
|
|
||
| <properties> | ||
| <!-- override module name defined in parent ("-" is not allowed) --> | ||
| <module-name>${groupId}.codereadiness</module-name> | ||
| </properties> | ||
|
|
||
| <dependencies> | ||
| <dependency> | ||
| <groupId>org.semver4j</groupId> | ||
| <artifactId>semver4j</artifactId> | ||
| <version>5.8.0</version> | ||
| </dependency> | ||
| </dependencies> | ||
| </project> |
95 changes: 95 additions & 0 deletions
95
...eadiness/src/main/java/dev/openfeature/contrib/hooks/codereadiness/CodeReadinessHook.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,95 @@ | ||
| package dev.openfeature.contrib.hooks.codereadiness; | ||
|
|
||
| import dev.openfeature.sdk.FlagEvaluationDetails; | ||
| import dev.openfeature.sdk.Hook; | ||
| import dev.openfeature.sdk.HookContext; | ||
| import dev.openfeature.sdk.ImmutableMetadata; | ||
| import dev.openfeature.sdk.exceptions.GeneralError; | ||
|
|
||
| import java.util.Map; | ||
| import java.util.Objects; | ||
|
|
||
| import lombok.Builder; | ||
| import lombok.extern.slf4j.Slf4j; | ||
|
|
||
| @Builder(builderMethodName = "", builderClassName = "Builder") | ||
| @Slf4j | ||
| public class CodeReadinessHook implements Hook { | ||
|
|
||
| private static final String DEFAULT_MIN_CODE_VERSION_KEY = "minCodeVersion"; | ||
| private static final boolean DEFAULT_STRICT_VALIDATION = false; | ||
| private static final VersionComparator DEFAULT_VERSION_COMPARATOR = new SemVerComparator(); | ||
|
|
||
| private final String currentVersion; | ||
|
|
||
| @Builder.Default | ||
| private final boolean strictValidation = DEFAULT_STRICT_VALIDATION; | ||
|
|
||
| @Builder.Default | ||
| private final String metadataMinVerKey = DEFAULT_MIN_CODE_VERSION_KEY; | ||
|
|
||
| @Builder.Default | ||
| private final VersionComparator comparator = DEFAULT_VERSION_COMPARATOR; | ||
|
|
||
| CodeReadinessHook(String currentVersion, boolean strictValidation, String metadataMinVerKey, VersionComparator comparator) { | ||
| this.currentVersion = Objects.requireNonNull(currentVersion, "codereadiness: currentVersion cannot be null"); | ||
| this.strictValidation = strictValidation; | ||
| this.metadataMinVerKey = Objects.requireNonNull(metadataMinVerKey, "codereadiness: metadataMinVerKey cannot be null"); | ||
| this.comparator = Objects.requireNonNull(comparator, "codereadiness: comparator cannot be null"); | ||
| } | ||
|
|
||
| public static Builder builder(String currentVersion) { | ||
| Objects.requireNonNull(currentVersion, "codereadiness: currentVersion cannot be null"); | ||
| return new Builder().currentVersion(currentVersion); | ||
| } | ||
|
|
||
| @Override | ||
| public void after(HookContext ctx, FlagEvaluationDetails details, Map hints) { | ||
| ImmutableMetadata metadata = details != null ? details.getFlagMetadata() : null; | ||
| if (metadata == null || metadata.isEmpty()) { | ||
| if (strictValidation) { | ||
| throw new GeneralError(String.format("flag metadata is null for flag \"%s\"", ctx.getFlagKey())); | ||
| } | ||
| log.debug("flag metadata is null for flag \"{}\", skipping validation", ctx.getFlagKey()); | ||
| return; | ||
| } | ||
| Object minVerObj = metadata.asUnmodifiableMap().get(metadataMinVerKey); | ||
| if (minVerObj == null) { | ||
| if (strictValidation) { | ||
| throw new GeneralError(String.format("key \"%s\" missing in flag's \"%s\" metadata", metadataMinVerKey, ctx.getFlagKey())); | ||
| } | ||
| log.debug("key \"{}\" missing in flag's \"{}\" metadata, skipping validation", metadataMinVerKey, ctx.getFlagKey()); | ||
| return; | ||
| } | ||
| if (!(minVerObj instanceof String)) { | ||
| if (strictValidation) { | ||
| throw new GeneralError(String.format("metadata \"%s\" is not a string for flag \"%s\"", metadataMinVerKey, ctx.getFlagKey())); | ||
| } | ||
| log.debug("metadata \"{}\" is not a string for flag \"{}\", skipping validation", metadataMinVerKey, ctx.getFlagKey()); | ||
| return; | ||
| } | ||
| String minCodeVersion = (String) minVerObj; | ||
| if (minCodeVersion.isEmpty()) { | ||
| if (strictValidation) { | ||
| throw new GeneralError(String.format("metadata \"%s\" is empty for flag \"%s\"", metadataMinVerKey, ctx.getFlagKey())); | ||
| } | ||
| log.debug("metadata \"{}\" is empty for flag \"{}\", skipping validation", metadataMinVerKey, ctx.getFlagKey()); | ||
| return; | ||
| } | ||
| boolean isCodeReady; | ||
| try { | ||
| isCodeReady = comparator.compare(currentVersion, minCodeVersion); | ||
| } catch (Exception err) { | ||
| throw new GeneralError( | ||
| String.format( | ||
| "current version: \"%s\" required minimum version: \"%s\" check failed: %s", | ||
| currentVersion, minCodeVersion, err.getMessage()), | ||
| err); | ||
| } | ||
| if (!isCodeReady) { | ||
| throw new GeneralError(String.format("current version: \"%s\" required minimum version: \"%s\" check failed", currentVersion, minCodeVersion)); | ||
| } | ||
| } | ||
|
|
||
|
|
||
| } |
29 changes: 29 additions & 0 deletions
29
...readiness/src/main/java/dev/openfeature/contrib/hooks/codereadiness/SemVerComparator.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,29 @@ | ||
| package dev.openfeature.contrib.hooks.codereadiness; | ||
|
|
||
| import dev.openfeature.sdk.FlagEvaluationDetails; | ||
| import dev.openfeature.sdk.Hook; | ||
| import dev.openfeature.sdk.HookContext; | ||
| import dev.openfeature.sdk.ImmutableMetadata; | ||
| import dev.openfeature.sdk.exceptions.GeneralError; | ||
| import java.util.Map; | ||
| import org.semver4j.Semver; | ||
|
|
||
| public class SemVerComparator implements VersionComparator { | ||
| @Override | ||
| public boolean compare(String currentVersion, String minCodeVersion) throws Exception { | ||
| String currentFormatted = currentVersion != null && currentVersion.startsWith("v") ? currentVersion : "v" + currentVersion; | ||
| String minCodeVersionFormatted = minCodeVersion != null && minCodeVersion.startsWith("v") ? minCodeVersion : "v" + minCodeVersion; | ||
|
|
||
| Semver currentSemver = Semver.parse(currentFormatted); | ||
| if (currentSemver == null) { | ||
| throw new IllegalArgumentException(String.format("invalid current semver: \"%s\"", currentVersion)); | ||
| } | ||
|
|
||
| Semver minCodeVersionSemver = Semver.parse(minCodeVersionFormatted); | ||
| if (minCodeVersionSemver == null) { | ||
| throw new IllegalArgumentException(String.format("invalid min code version semver: \"%s\"", minCodeVersion)); | ||
| } | ||
|
|
||
| return currentSemver.isGreaterThan(minCodeVersionSemver) || currentSemver.isEqualTo(minCodeVersionSemver); | ||
| } | ||
| } |
21 changes: 21 additions & 0 deletions
21
...eadiness/src/main/java/dev/openfeature/contrib/hooks/codereadiness/VersionComparator.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,21 @@ | ||
| package dev.openfeature.contrib.hooks.codereadiness; | ||
|
|
||
| /** | ||
| * Defines the contract for comparing code version strings (current and minimum required | ||
| * version) according to specified rules. Used by {@link CodeReadinessHook}. | ||
| * | ||
| * <p>The {@link CodeReadinessHook} uses {@link SemVerComparator} by default for standard Semantic | ||
| * Versioning, but developers may implement this interface to support custom or non-standard | ||
| * versioning schemes. | ||
| */ | ||
| public interface VersionComparator { | ||
| /** | ||
| * Compare current version with required version. | ||
| * | ||
| * @param currentVersion of the application | ||
| * @param minCodeVersion required minimum version | ||
| * @return true if currentVersion is greater than or equal to minCodeVersion | ||
| * @throws Exception if there is a parsing error | ||
| */ | ||
| boolean compare(String currentVersion, String minCodeVersion) throws Exception; | ||
| } | ||
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.