-
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
Open
marcin11858
wants to merge
3
commits 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.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
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,95 @@ | ||
| # 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 int-based versioning or custom build numbers), a custom comparison interface implementation can be provided using `comparator()`. The `VersionComparator<T>` interface separates version string parsing from comparison logic. | ||
|
|
||
| ```java | ||
| import dev.openfeature.contrib.hooks.codereadiness.VersionComparator; | ||
|
|
||
| // Example for int-based versioning | ||
| VersionComparator<Integer> intComparator = new VersionComparator<Integer>() { | ||
|
|
||
| @Override | ||
| public Integer parse(String versionString) { | ||
| return Integer.parseInt(versionString); | ||
| } | ||
|
|
||
| @Override | ||
| public boolean compare(Integer current, Integer required) { | ||
| return current >= required; | ||
| } | ||
| }; | ||
|
|
||
| CodeReadinessHook codeReadinessHook = CodeReadinessHook.builder("15") | ||
| .comparator(intComparator) | ||
| .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> |
106 changes: 106 additions & 0 deletions
106
...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,106 @@ | ||
| 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.extern.slf4j.Slf4j; | ||
|
|
||
| /** | ||
| * Hook for controlling feature flag evaluation based on the application code version. | ||
| */ | ||
| @Slf4j | ||
| public final class CodeReadinessHook implements Hook { | ||
|
|
||
| private static final String DEFAULT_MIN_CODE_VERSION_KEY = "minCodeVersion"; | ||
| private static final boolean DEFAULT_STRICT_VALIDATION = false; | ||
|
|
||
| private final String currentVersion; | ||
| private final boolean strictValidation; | ||
| private final String metadataMinVerKey; | ||
| private final VersionComparator<Object> comparator; | ||
| private final Object parsedCurrentVersion; | ||
|
|
||
| /** | ||
| * Builder for {@link CodeReadinessHook}. | ||
| */ | ||
| public static class Builder { | ||
| String currentVersion; | ||
| boolean strictValidation = DEFAULT_STRICT_VALIDATION; | ||
| String metadataMinVerKey = DEFAULT_MIN_CODE_VERSION_KEY; | ||
| VersionComparator<?> comparator = new SemVerComparator(); | ||
| } | ||
|
|
||
| @lombok.Builder(builderMethodName = "", builderClassName = "Builder") | ||
| 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"); | ||
| Objects.requireNonNull(comparator, "codereadiness: comparator cannot be null"); | ||
| this.comparator = (VersionComparator<Object>) comparator; | ||
| try { | ||
| this.parsedCurrentVersion = this.comparator.parse(currentVersion); | ||
| } catch (Exception err) { | ||
| throw new GeneralError( | ||
| String.format( | ||
| "current version: \"%s\" initialization failed: %s", currentVersion, err.getMessage()), | ||
| err); | ||
| } | ||
| } | ||
|
|
||
| public static Builder builder(String currentVersion) { | ||
|
marcin11858 marked this conversation as resolved.
|
||
| 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; | ||
| } | ||
| String minCodeVersion = metadata.getString(metadataMinVerKey); | ||
| if (minCodeVersion == null || minCodeVersion.isEmpty()) { | ||
| if (strictValidation) { | ||
| throw new GeneralError(String.format( | ||
| "key \"%s\" missing or empty in flag's \"%s\" metadata", metadataMinVerKey, ctx.getFlagKey())); | ||
| } | ||
| log.debug( | ||
| "key \"{}\" missing or empty in flag's \"{}\", skipping validation", | ||
| metadataMinVerKey, | ||
| ctx.getFlagKey()); | ||
| return; | ||
| } | ||
| boolean isCodeReady; | ||
| try { | ||
| Object parsedMinVersion = comparator.parse(minCodeVersion); | ||
| isCodeReady = comparator.compare(this.parsedCurrentVersion, parsedMinVersion); | ||
| } catch (Exception err) { | ||
| if (strictValidation) { | ||
| throw new GeneralError( | ||
| String.format( | ||
| "current version: \"%s\" required minimum version: \"%s\" check failed: %s", | ||
| currentVersion, minCodeVersion, err.getMessage()), | ||
| err); | ||
| } | ||
| log.debug(String.format("invalid version values for flag \"%s\", skipping validation", ctx.getFlagKey())); | ||
| return; | ||
| } | ||
| 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 java.util.Objects; | ||
| import org.semver4j.Semver; | ||
|
|
||
| /** | ||
| * Default comparator implementation for standard Semantic Versioning (SemVer). | ||
| */ | ||
| public class SemVerComparator implements VersionComparator<Semver> { | ||
|
|
||
| public SemVerComparator() {} | ||
|
|
||
| @Override | ||
| public Semver parse(String versionString) { | ||
| Objects.requireNonNull(versionString, "versionString cannot be null"); | ||
| Semver semver = Semver.parse(versionString); | ||
| if (semver == null) { | ||
| throw new IllegalArgumentException(String.format("invalid semver: \"%s\"", versionString)); | ||
| } | ||
| return semver; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean compare(Semver currentVersion, Semver minCodeVersion) { | ||
| Objects.requireNonNull(currentVersion, "currentVersion cannot be null"); | ||
| Objects.requireNonNull(minCodeVersion, "minCodeVersion cannot be null"); | ||
| return currentVersion.isGreaterThanOrEqualTo(minCodeVersion); | ||
| } | ||
| } |
24 changes: 24 additions & 0 deletions
24
...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,24 @@ | ||
| package dev.openfeature.contrib.hooks.codereadiness; | ||
|
|
||
| /** | ||
| * Defines the contract for parsing version strings and comparing code version objects | ||
| * (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. | ||
| * | ||
| * @param <T> The domain object type representing a parsed version (e.g., Semver, LocalDate, Integer). | ||
| */ | ||
| public interface VersionComparator<T> { | ||
|
|
||
| /** | ||
| * Parse version string into domain object. | ||
| */ | ||
| T parse(String versionString) throws Exception; | ||
|
|
||
| /** | ||
| * Compare current version with required version. | ||
| */ | ||
| boolean compare(T currentVersion, T minCodeVersion) throws Exception; | ||
|
chrfwow marked this conversation as resolved.
|
||
| } | ||
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.