feat(codereadiness): add hook for version-based flag validation · open-feature/java-sdk-contrib@51fbe31 · GitHub
Skip to content

Commit 51fbe31

Browse files
committed
feat(codereadiness): add hook for version-based flag validation
Introduce the codereadiness hook to control feature flag evaluation by comparing the application's current version with a required minimum version specified in the flag's metadata. If the comparator returns false the hook returns an error to trigger fallback to the default flag value. Signed-off-by: Marcin Wlazły <marcinwlazly@google.com>
1 parent 3860e27 commit 51fbe31

10 files changed

Lines changed: 656 additions & 0 deletions

File tree

‎.release-please-manifest.json‎

Lines changed: 1 addition & 0 deletions

‎hooks/codereadiness/README.md‎

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Code Readiness Hook
2+
3+
The `codereadiness` hook allows controlling feature flag evaluation based on the version of the application code.
4+
It does this by comparing the current application version with a required minimum version specified in the flag's metadata.
5+
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.
6+
7+
## Installation
8+
9+
```xml
10+
<dependency>
11+
<groupId>dev.openfeature.contrib.hooks</groupId>
12+
<artifactId>code-readiness-hook</artifactId>
13+
<version>0.1.0</version>
14+
</dependency>
15+
```
16+
17+
## Setup
18+
19+
First, import the OpenFeature SDK and the code readiness hook:
20+
21+
```java
22+
import dev.openfeature.sdk.OpenFeatureAPI;
23+
import dev.openfeature.contrib.hooks.codereadiness.CodeReadinessHook;
24+
```
25+
26+
Then, configure the hook with the current version of the application code and register it:
27+
28+
```java
29+
// currentVersion is the current version of the code, which can be retrieved
30+
// from environment variables, build properties, or configuration files.
31+
String currentVersion = "1.0.0";
32+
33+
CodeReadinessHook codeReadinessHook = CodeReadinessHook.builder(currentVersion).build();
34+
35+
// Register the hook globally at the OpenFeature API level
36+
OpenFeatureAPI.getInstance().addHooks(codeReadinessHook);
37+
```
38+
39+
## How It Works
40+
41+
1. The hook runs during the **After** phase of flag evaluation.
42+
2. It extracts the metadata associated with the evaluated flag.
43+
3. It looks for a specific metadata key (by default, `minCodeVersion`).
44+
4. If found, it compares the current application version against the required minimum version using the configured comparator (by default, a semver comparison).
45+
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.
46+
47+
## Options
48+
49+
The behavior of the hook can be customized by passing options to the builder:
50+
51+
### Strict Validation
52+
53+
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)`.
54+
55+
```java
56+
CodeReadinessHook codeReadinessHook = CodeReadinessHook.builder("1.0.0")
57+
.strictValidation(true)
58+
.build();
59+
```
60+
61+
### Custom Metadata Key
62+
63+
To configure the hook to look for a key other than the default `"minCodeVersion"` in the flag's metadata, use `metadataMinVerKey()`.
64+
65+
```java
66+
CodeReadinessHook codeReadinessHook = CodeReadinessHook.builder("1.0.0")
67+
.metadataMinVerKey("customMetadataKey")
68+
.build();
69+
```
70+
71+
### Custom Comparator
72+
73+
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.
74+
75+
```java
76+
import dev.openfeature.contrib.hooks.codereadiness.VersionComparator;
77+
78+
// Example for int-based versioning
79+
VersionComparator<Integer> intComparator = new VersionComparator<Integer>() {
80+
81+
@Override
82+
public Integer parse(String versionString) {
83+
return Integer.parseInt(versionString);
84+
}
85+
86+
@Override
87+
public boolean compare(Integer current, Integer required) {
88+
return current >= required;
89+
}
90+
};
91+
92+
CodeReadinessHook codeReadinessHook = CodeReadinessHook.builder("15")
93+
.comparator(intComparator)
94+
.build();
95+
```

‎hooks/codereadiness/pom.xml‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4+
<modelVersion>4.0.0</modelVersion>
5+
<parent>
6+
<groupId>dev.openfeature.contrib</groupId>
7+
<artifactId>parent</artifactId>
8+
<version>[1.0,2.0)</version>
9+
<relativePath>../../pom.xml</relativePath>
10+
</parent>
11+
<groupId>dev.openfeature.contrib.hooks</groupId>
12+
<artifactId>code-readiness-hook</artifactId>
13+
<version>0.1.0</version> <!--x-release-please-version -->
14+
15+
<name>code-readiness-hook</name>
16+
<description>Code Readiness Hook</description>
17+
<url>https://openfeature.dev</url>
18+
19+
<properties>
20+
<!-- override module name defined in parent ("-" is not allowed) -->
21+
<module-name>${groupId}.codereadiness</module-name>
22+
</properties>
23+
24+
<dependencies>
25+
<dependency>
26+
<groupId>org.semver4j</groupId>
27+
<artifactId>semver4j</artifactId>
28+
<version>5.8.0</version>
29+
</dependency>
30+
</dependencies>
31+
</project>
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package dev.openfeature.contrib.hooks.codereadiness;
2+
3+
import dev.openfeature.sdk.FlagEvaluationDetails;
4+
import dev.openfeature.sdk.Hook;
5+
import dev.openfeature.sdk.HookContext;
6+
import dev.openfeature.sdk.ImmutableMetadata;
7+
import dev.openfeature.sdk.exceptions.GeneralError;
8+
import java.util.Map;
9+
import java.util.Objects;
10+
import lombok.Builder;
11+
import lombok.extern.slf4j.Slf4j;
12+
13+
/**
14+
* Hook for controlling feature flag evaluation based on the application code version.
15+
*/
16+
@Slf4j
17+
public class CodeReadinessHook implements Hook {
18+
19+
private static final String DEFAULT_MIN_CODE_VERSION_KEY = "minCodeVersion";
20+
private static final boolean DEFAULT_STRICT_VALIDATION = false;
21+
22+
private final String currentVersion;
23+
private final boolean strictValidation;
24+
private final String metadataMinVerKey;
25+
private final VersionComparator<Object> comparator;
26+
private final Object parsedCurrentVersion;
27+
28+
/**
29+
* Builder for {@link CodeReadinessHook}.
30+
*/
31+
public static class Builder {
32+
private String currentVersion;
33+
private boolean strictValidation = DEFAULT_STRICT_VALIDATION;
34+
private String metadataMinVerKey = DEFAULT_MIN_CODE_VERSION_KEY;
35+
private VersionComparator<?> comparator = new SemVerComparator();
36+
}
37+
38+
@lombok.Builder(builderMethodName = "", builderClassName = "Builder")
39+
CodeReadinessHook(String currentVersion, boolean strictValidation, String metadataMinVerKey,
40+
VersionComparator<?> comparator) {
41+
this.currentVersion = Objects.requireNonNull(currentVersion, "codereadiness: currentVersion cannot be null");
42+
this.strictValidation = strictValidation;
43+
this.metadataMinVerKey = Objects.requireNonNull(metadataMinVerKey,
44+
"codereadiness: metadataMinVerKey cannot be null");
45+
Objects.requireNonNull(comparator, "codereadiness: comparator cannot be null");
46+
this.comparator = (VersionComparator<Object>) comparator;
47+
try {
48+
this.parsedCurrentVersion = this.comparator.parse(currentVersion);
49+
} catch (Exception err) {
50+
throw new GeneralError(
51+
String.format(
52+
"current version: \"%s\" initialization failed: %s",
53+
currentVersion, err.getMessage()),
54+
err);
55+
}
56+
}
57+
58+
public static Builder builder(String currentVersion) {
59+
return new Builder().currentVersion(currentVersion);
60+
}
61+
62+
@Override
63+
public void after(HookContext ctx, FlagEvaluationDetails details, Map hints) {
64+
ImmutableMetadata metadata = details != null ? details.getFlagMetadata() : null;
65+
if (metadata == null || metadata.isEmpty()) {
66+
if (strictValidation) {
67+
throw new GeneralError(String.format("flag metadata is null for flag \"%s\"", ctx.getFlagKey()));
68+
}
69+
log.debug("flag metadata is null for flag \"{}\", skipping validation", ctx.getFlagKey());
70+
return;
71+
}
72+
String minCodeVersion = metadata.getString(metadataMinVerKey);
73+
if (minCodeVersion == null || minCodeVersion.isEmpty()) {
74+
if (strictValidation) {
75+
throw new GeneralError(String.format("key \"%s\" missing or empty in flag's \"%s\" metadata",
76+
metadataMinVerKey, ctx.getFlagKey()));
77+
}
78+
log.debug("key \"{}\" missing or empty in flag's \"{}\", skipping validation",
79+
metadataMinVerKey, ctx.getFlagKey());
80+
return;
81+
}
82+
boolean isCodeReady;
83+
try {
84+
Object parsedMinVersion = comparator.parse(minCodeVersion);
85+
isCodeReady = comparator.compare(this.parsedCurrentVersion, parsedMinVersion);
86+
} catch (Exception err) {
87+
if (strictValidation) {
88+
throw new GeneralError(
89+
String.format(
90+
"current version: \"%s\" required minimum version: \"%s\" check failed: %s",
91+
currentVersion, minCodeVersion, err.getMessage()),
92+
err);
93+
}
94+
log.debug(String.format("invalid version values for flag \"%s\", skipping validation", ctx.getFlagKey()));
95+
return;
96+
}
97+
if (!isCodeReady) {
98+
throw new GeneralError(
99+
String.format("current version: \"%s\" required minimum version: \"%s\" check failed",
100+
currentVersion, minCodeVersion));
101+
}
102+
}
103+
104+
105+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package dev.openfeature.contrib.hooks.codereadiness;
2+
3+
import java.util.Objects;
4+
import org.semver4j.Semver;
5+
6+
/**
7+
* Default comparator implementation for standard Semantic Versioning (SemVer).
8+
*/
9+
public class SemVerComparator implements VersionComparator<Semver> {
10+
11+
public SemVerComparator() {
12+
}
13+
14+
@Override
15+
public Semver parse(String versionString) {
16+
Objects.requireNonNull(versionString, "versionString cannot be null");
17+
Semver semver = Semver.parse(versionString);
18+
if (semver == null) {
19+
throw new IllegalArgumentException(String.format("invalid semver: \"%s\"", versionString));
20+
}
21+
return semver;
22+
}
23+
24+
@Override
25+
public boolean compare(Semver currentVersion, Semver minCodeVersion) {
26+
Objects.requireNonNull(currentVersion, "currentVersion cannot be null");
27+
Objects.requireNonNull(minCodeVersion, "minCodeVersion cannot be null");
28+
return currentVersion.isGreaterThanOrEqualTo(minCodeVersion);
29+
}
30+
}
Lines changed: 33 additions & 0 deletions

0 commit comments

Comments
 (0)