operands() {
- return Set.of(expr());
- }
-
- public Expr operand() {
- return operands().iterator().next();
- }
-
- @Override
- public String toString() {
- return name();
- }
-}
diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/OpenFeatureLocalResolveProvider.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/OpenFeatureLocalResolveProvider.java
index 098345c9..199976a2 100644
--- a/openfeature-provider-local/src/main/java/com/spotify/confidence/OpenFeatureLocalResolveProvider.java
+++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/OpenFeatureLocalResolveProvider.java
@@ -22,24 +22,9 @@
/**
* OpenFeature provider for Confidence feature flags using local resolution.
*
- * This provider evaluates feature flags locally using either a WebAssembly (WASM) resolver or a
- * pure Java implementation. It periodically syncs flag configurations from the Confidence service
- * and caches them locally for fast, low-latency flag evaluation.
- *
- *
The provider supports two resolution modes:
- *
- *
- * - WASM mode (default): Uses a WebAssembly resolver
- *
- Java mode: Uses a pure Java resolver
- *
- *
- * Resolution mode can be controlled via the {@code LOCAL_RESOLVE_MODE} environment variable:
- *
- *
- * - {@code LOCAL_RESOLVE_MODE=WASM} - Forces WASM mode
- *
- {@code LOCAL_RESOLVE_MODE=JAVA} - Forces Java mode
- *
- Not set - Defaults to WASM mode
- *
+ * This provider evaluates feature flags locally using either a WebAssembly (WASM) resolver. It
+ * periodically syncs flag configurations from the Confidence service and caches them locally for
+ * fast, low-latency flag evaluation.
*
*
Usage Example:
*
@@ -71,16 +56,13 @@ public class OpenFeatureLocalResolveProvider implements FeatureProvider {
private final StickyResolveStrategy stickyResolveStrategy;
/**
- * Creates a new OpenFeature provider for local flag resolution with default fallback strategy and
- * no retry.
+ * Creates a new OpenFeature provider for local flag resolution with sticky default fallback
+ * strategy and no retry.
*
*
This constructor uses {@link RemoteResolverFallback} as the default sticky resolve strategy,
* which provides fallback to the remote Confidence service when the WASM resolver encounters
* missing materializations. By default, no retry strategy is applied.
*
- *
The provider will automatically determine the resolution mode (WASM or Java) based on the
- * {@code LOCAL_RESOLVE_MODE} environment variable, defaulting to WASM mode.
- *
* @param apiSecret the API credentials containing client ID and client secret for authenticating
* with the Confidence service. Create using {@code new ApiSecret("client-id",
* "client-secret")}
@@ -97,8 +79,7 @@ public OpenFeatureLocalResolveProvider(ApiSecret apiSecret, String clientSecret)
* Creates a new OpenFeature provider for local flag resolution with full configuration control.
*
*
This is the primary constructor that allows full control over the provider configuration,
- * including retry strategy. The provider will automatically determine the resolution mode (WASM
- * or Java) based on the {@code LOCAL_RESOLVE_MODE} environment variable, defaulting to WASM mode.
+ * including retry strategy.
*
* @param apiSecret the API credentials containing client ID and client secret for authenticating
* with the Confidence service. Create using {@code new ApiSecret("client-id",
@@ -111,19 +92,9 @@ public OpenFeatureLocalResolveProvider(ApiSecret apiSecret, String clientSecret)
*/
public OpenFeatureLocalResolveProvider(
ApiSecret apiSecret, String clientSecret, StickyResolveStrategy stickyResolveStrategy) {
- final var env = System.getenv("LOCAL_RESOLVE_MODE");
- if (env != null && env.equals("WASM")) {
- this.flagResolverService =
- LocalResolverServiceFactory.from(apiSecret, clientSecret, true, stickyResolveStrategy);
- } else if (env != null && env.equals("JAVA")) {
- this.flagResolverService =
- LocalResolverServiceFactory.from(apiSecret, clientSecret, false, stickyResolveStrategy);
- } else {
- this.flagResolverService =
- LocalResolverServiceFactory.from(apiSecret, clientSecret, true, stickyResolveStrategy);
- }
- this.stickyResolveStrategy = stickyResolveStrategy;
+ this.flagResolverService = LocalResolverServiceFactory.from(apiSecret, stickyResolveStrategy);
this.clientSecret = clientSecret;
+ this.stickyResolveStrategy = stickyResolveStrategy;
}
/**
diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/Or.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/Or.java
deleted file mode 100644
index de348c5a..00000000
--- a/openfeature-provider-local/src/main/java/com/spotify/confidence/Or.java
+++ /dev/null
@@ -1,37 +0,0 @@
-package com.spotify.confidence;
-
-import static java.util.stream.Collectors.toSet;
-
-import java.util.Set;
-import java.util.stream.Stream;
-
-record Or(Set operands) implements AndOr {
-
- @Override
- public Type type() {
- return Type.OR;
- }
-
- @Override
- public Expr simplify() {
- final Set reduced =
- reduceNegatedPairsTo(T)
- .filter(o -> !o.isFalse())
- .flatMap(o -> o.isOr() ? o.operands().stream() : Stream.of(o))
- .collect(toSet());
-
- if (reduced.contains(T)) {
- return T;
- } else if (reduced.size() == 1) {
- return reduced.iterator().next();
- } else if (reduced.isEmpty()) {
- return F;
- }
- return Expr.or(reduced);
- }
-
- @Override
- public String toString() {
- return name();
- }
-}
diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/Ord.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/Ord.java
deleted file mode 100644
index 231fb37e..00000000
--- a/openfeature-provider-local/src/main/java/com/spotify/confidence/Ord.java
+++ /dev/null
@@ -1,66 +0,0 @@
-package com.spotify.confidence;
-
-import static com.google.protobuf.util.Timestamps.toNanos;
-
-import com.spotify.confidence.shaded.flags.types.v1.Targeting;
-
-interface Ord {
-
- boolean lt(Targeting.Value a, Targeting.Value b);
-
- boolean lte(Targeting.Value a, Targeting.Value b);
-
- static Ord getOrd(Targeting.Value value) {
- switch (value.getValueCase()) {
- case NUMBER_VALUE:
- return new Ord() {
- @Override
- public boolean lt(Targeting.Value a, Targeting.Value b) {
- return a.getNumberValue() < b.getNumberValue();
- }
-
- @Override
- public boolean lte(Targeting.Value a, Targeting.Value b) {
- return a.getNumberValue() <= b.getNumberValue();
- }
- };
- case TIMESTAMP_VALUE:
- return new Ord() {
- @Override
- public boolean lt(Targeting.Value a, Targeting.Value b) {
- return toNanos(a.getTimestampValue()) < toNanos(b.getTimestampValue());
- }
-
- @Override
- public boolean lte(Targeting.Value a, Targeting.Value b) {
- return toNanos(a.getTimestampValue()) <= toNanos(b.getTimestampValue());
- }
- };
- case VERSION_VALUE:
- return new Ord() {
- @Override
- public boolean lt(Targeting.Value a, Targeting.Value b) {
- final SemanticVersion versionA =
- SemanticVersion.fromVersionString(a.getVersionValue().getVersion());
- final SemanticVersion versionB =
- SemanticVersion.fromVersionString(b.getVersionValue().getVersion());
- return versionA.compareTo(versionB) < 0;
- }
-
- @Override
- public boolean lte(Targeting.Value a, Targeting.Value b) {
- final SemanticVersion versionA =
- SemanticVersion.fromVersionString(a.getVersionValue().getVersion());
- final SemanticVersion versionB =
- SemanticVersion.fromVersionString(b.getVersionValue().getVersion());
- return versionA.compareTo(versionB) <= 0;
- }
- };
-
- case BOOL_VALUE:
- case STRING_VALUE:
- default:
- throw new UnsupportedOperationException(value.getValueCase() + " is not comparable");
- }
- }
-}
diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/PlainResolveTokenConverter.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/PlainResolveTokenConverter.java
deleted file mode 100644
index 8bcdc707..00000000
--- a/openfeature-provider-local/src/main/java/com/spotify/confidence/PlainResolveTokenConverter.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package com.spotify.confidence;
-
-import com.google.protobuf.ByteString;
-import com.google.protobuf.InvalidProtocolBufferException;
-import com.spotify.confidence.shaded.flags.resolver.v1.ResolveToken;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-class PlainResolveTokenConverter extends ResolveTokenConverter {
-
- private static final Logger logger = LoggerFactory.getLogger(PlainResolveTokenConverter.class);
-
- @Override
- public ByteString convertResolveToken(ResolveToken resolveToken) {
- return resolveToken.toByteString();
- }
-
- @Override
- public ResolveToken readResolveToken(ByteString tokenBytes) {
- try {
- return ResolveToken.parseFrom(tokenBytes);
- } catch (InvalidProtocolBufferException e) {
- logger.warn("Got InvalidProtocolBufferException when reading resolve token", e);
- throw new BadRequestException("Unable to parse resolve token");
- }
- }
-}
diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/Randomizer.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/Randomizer.java
deleted file mode 100644
index 027b51b3..00000000
--- a/openfeature-provider-local/src/main/java/com/spotify/confidence/Randomizer.java
+++ /dev/null
@@ -1,55 +0,0 @@
-package com.spotify.confidence;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import com.google.common.base.Joiner;
-import com.google.common.hash.HashCode;
-import com.google.common.hash.HashFunction;
-import com.google.common.hash.Hashing;
-import com.spotify.confidence.shaded.flags.admin.v1.Flag.Rule.Assignment;
-import com.spotify.confidence.shaded.flags.admin.v1.Flag.Rule.BucketRange;
-import java.util.BitSet;
-
-class Randomizer {
-
- static final String MEGA_SALT = "MegaSalt";
- static final int BR_BUCKET_COUNT = 1_000_000;
- static final BitSet FULL_BITSET = new BitSet(BR_BUCKET_COUNT);
-
- static {
- for (int i = 0; i < BR_BUCKET_COUNT; i++) {
- FULL_BITSET.set(i);
- }
- }
-
- private static final HashFunction HASH_FUNCTION = Hashing.murmur3_128();
- private static final Joiner PIPE_JOINER = Joiner.on('|');
- private static final long LONG_SCALE = 0x0FFF_FFFF_FFFF_FFFFL;
-
- static boolean inBitset(AccountState state, String segmentName, String unit) {
- if (!state.bitsets().containsKey(segmentName) || state.bitsets().get(segmentName).isEmpty()) {
- return false;
- }
-
- final BitSet bitset = state.bitsets().get(segmentName);
- final long bucket = getBucket(unit, state.saltForAccount(), BR_BUCKET_COUNT);
-
- return bitset.get((int) bucket);
- }
-
- static long getBucket(String unit, String salt, int bucketCount) {
- final String[] data = new String[] {salt, unit};
- final String unitString = PIPE_JOINER.join(data);
- final HashCode hashCode = HASH_FUNCTION.hashString(unitString, UTF_8);
- return ((hashCode.asLong() >> 4) & LONG_SCALE) % bucketCount;
- }
-
- static boolean coversBucket(Assignment assignment, long bucket) {
- for (BucketRange range : assignment.getBucketRangesList()) {
- if (range.getLower() <= bucket && bucket < range.getUpper()) {
- return true;
- }
- }
- return false;
- }
-}
diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/Ref.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/Ref.java
deleted file mode 100644
index 7eb9bb09..00000000
--- a/openfeature-provider-local/src/main/java/com/spotify/confidence/Ref.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.spotify.confidence;
-
-import java.util.Set;
-
-record Ref(String name) implements Expr {
-
- @Override
- public Type type() {
- return Type.REF;
- }
-
- @Override
- public Set operands() {
- return Set.of();
- }
-
- @Override
- public String toString() {
- return name();
- }
-}
diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/Region.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/Region.java
deleted file mode 100644
index 835afd29..00000000
--- a/openfeature-provider-local/src/main/java/com/spotify/confidence/Region.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package com.spotify.confidence;
-
-enum Region {
- EU,
- US
-}
diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/ResolveFlagListener.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/ResolveFlagListener.java
deleted file mode 100644
index 016b6133..00000000
--- a/openfeature-provider-local/src/main/java/com/spotify/confidence/ResolveFlagListener.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package com.spotify.confidence;
-
-interface ResolveFlagListener {
-
- enum RuleEvaluationReason {
- RULE_MATCHED,
- RULE_EVALUATED_NO_VARIANT_MATCH,
- RULE_NOT_ENABLED,
- RULE_NOT_EVALUATED,
- SEGMENT_NOT_FOUND_OR_NOT_ACTIVE,
- SEGMENT_NOT_MATCHED,
- RULE_MATCHED_FALLTHROUGH,
- MATERIALIZATION_NOT_MATCHED,
- MATERIALIZATION_AND_SEGMENT_NOT_MATCHED
- }
-
- enum SegmentEvaluationReason {
- SEGMENT_MATCHED,
- SEGMENT_NOT_EVALUATED,
- TARGETING_NOT_MATCHED,
- BITSET_NOT_MATCHED,
- TARGETING_KEY_MISSING,
- }
-
- default void markRuleEvaluationReason(String rule, RuleEvaluationReason reason) {}
-
- default void markSegmentEvaluationReason(
- String flag, String segment, SegmentEvaluationReason reason) {}
-
- default void addEvalContextMissingValue(String flag, String segment, String fieldName) {}
-}
diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/ResolveLogger.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/ResolveLogger.java
deleted file mode 100644
index 68177af8..00000000
--- a/openfeature-provider-local/src/main/java/com/spotify/confidence/ResolveLogger.java
+++ /dev/null
@@ -1,267 +0,0 @@
-package com.spotify.confidence;
-
-import com.google.protobuf.Struct;
-import com.spotify.confidence.shaded.flags.admin.v1.ClientResolveInfo;
-import com.spotify.confidence.shaded.flags.admin.v1.FlagAdminServiceGrpc;
-import com.spotify.confidence.shaded.flags.admin.v1.WriteResolveInfoRequest;
-import java.io.Closeable;
-import java.time.Duration;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.Timer;
-import java.util.TimerTask;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.atomic.AtomicLong;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReadWriteLock;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
-import java.util.function.Supplier;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-class ResolveLogger implements Closeable {
- private static final Logger logger = LoggerFactory.getLogger(ResolveLogger.class);
- private final Supplier adminStub;
-
- private final AtomicReference stateRef =
- new AtomicReference<>(new ResolveInfoState());
- private final Timer timer;
-
- private ResolveLogger(
- Supplier adminStub, Timer timer) {
- this.adminStub = adminStub;
- this.timer = timer;
- }
-
- static ResolveLogger createStarted(
- Supplier adminStub,
- Duration checkpointInterval) {
- final Timer timer = new Timer("resolve-logger-timer", true);
- final ResolveLogger resolveLogger = new ResolveLogger(adminStub, timer);
-
- timer.scheduleAtFixedRate(
- new TimerTask() {
- @Override
- public void run() {
- try {
- resolveLogger.checkpoint();
- } catch (Exception ex) {
- logger.error("Could not checkpoint", ex);
- }
- }
- },
- checkpointInterval.toMillis(),
- checkpointInterval.toMillis());
- return resolveLogger;
- }
-
- private void checkpoint() {
- final var state = stateRef.getAndSet(new ResolveInfoState());
- final var lock = state.readWriteLock.writeLock();
- try {
- lock.lock();
- if (state.isEmpty()) {
- return;
- }
- adminStub
- .get()
- .writeResolveInfo(
- WriteResolveInfoRequest.newBuilder()
- .addAllClientResolveInfo(
- state.clientResolveInfo().entrySet().stream()
- .map(
- entry ->
- ClientResolveInfo.newBuilder()
- .setClient(extractClient(entry.getKey()))
- .setClientCredential(entry.getKey())
- .addAllSchema(
- entry.getValue().schemas().stream()
- .map(
- s ->
- ClientResolveInfo
- .EvaluationContextSchemaInstance
- .newBuilder()
- .putAllSchema(s.fields())
- .putAllSemanticTypes(s.semanticTypes())
- .build())
- .toList())
- .build())
- .toList())
- .addAllFlagResolveInfo(
- state.flagResolveInfo().entrySet().stream()
- .map(
- entry ->
- com.spotify.confidence.shaded.flags.admin.v1.FlagResolveInfo
- .newBuilder()
- .setFlag(entry.getKey())
- .addAllRuleResolveInfo(
- entry.getValue().ruleResolveInfo().entrySet().stream()
- .map(
- ruleInfo ->
- com.spotify.confidence.shaded.flags.admin.v1
- .FlagResolveInfo.RuleResolveInfo
- .newBuilder()
- .setRule(ruleInfo.getKey())
- .setCount(
- ruleInfo.getValue().count().get())
- .addAllAssignmentResolveInfo(
- ruleInfo
- .getValue()
- .assignmentCounts()
- .entrySet()
- .stream()
- .map(
- assignmentEntry ->
- com.spotify.confidence
- .shaded.flags.admin.v1
- .FlagResolveInfo
- .AssignmentResolveInfo
- .newBuilder()
- .setAssignmentId(
- assignmentEntry
- .getKey())
- .setCount(
- assignmentEntry
- .getValue()
- .get())
- .build())
- .toList())
- .build())
- .toList())
- .addAllVariantResolveInfo(
- entry.getValue().variantResolveInfo().entrySet().stream()
- .map(
- variantInfo ->
- com.spotify.confidence.shaded.flags.admin.v1
- .FlagResolveInfo.VariantResolveInfo
- .newBuilder()
- .setVariant(variantInfo.getKey())
- .setCount(
- variantInfo.getValue().count().get())
- .build())
- .toList())
- .build())
- .toList())
- .build());
-
- } finally {
- lock.unlock();
- }
- }
-
- private String extractClient(String key) {
- final String[] split = key.split("/");
- return split[0] + "/" + split[1];
- }
-
- void logResolve(
- String resolveId,
- Struct evaluationContext,
- AccountClient accountClient,
- List values) {
- ResolveInfoState state = stateRef.get();
- Lock lock = state.readWriteLock.readLock();
- try {
- if (!lock.tryLock()) {
- // If we failed to lock it means that the checkpoint is currently running, which means that
- // the state has just been reset, so we can take the new state and lock on that
- state = stateRef.get();
- lock = state.readWriteLock.readLock();
- lock.lock();
- }
-
- final SchemaFromEvaluationContext.DerivedClientSchema derivedSchema =
- SchemaFromEvaluationContext.getSchema(evaluationContext);
- state
- .clientResolveInfo(accountClient.clientCredential().getName())
- .schemas()
- .add(derivedSchema);
-
- for (ResolvedValue value : values) {
- final var flagState = state.flagResolveInfo(value.flag().getName());
- for (var fallthrough : value.fallthroughAssignments()) {
- flagState.ruleResolveInfo(fallthrough.getRule()).increment(fallthrough.getAssignmentId());
- }
-
- if (value.matchedAssignment().isEmpty()) {
- flagState.variantResolveInfo("").increment();
- } else {
- final var match = value.matchedAssignment().get();
- flagState.variantResolveInfo(match.variant().orElse("")).increment();
- flagState.ruleResolveInfo(match.matchedRule().getName()).increment(match.assignmentId());
- }
- }
- } finally {
- lock.unlock();
- }
- }
-
- @Override
- public void close() {
- timer.cancel();
- checkpoint();
- }
-
- private record ResolveInfoState(
- ConcurrentMap flagResolveInfo,
- ConcurrentMap clientResolveInfo,
- ReadWriteLock readWriteLock) {
- ResolveInfoState() {
- this(new ConcurrentHashMap<>(), new ConcurrentHashMap<>(), new ReentrantReadWriteLock());
- }
-
- FlagResolveInfo flagResolveInfo(String flag) {
- return flagResolveInfo.computeIfAbsent(flag, ignore -> new FlagResolveInfo());
- }
-
- ClientResolveInfoState clientResolveInfo(String clientCredential) {
- return clientResolveInfo.computeIfAbsent(
- clientCredential, ignore -> new ClientResolveInfoState());
- }
-
- boolean isEmpty() {
- return flagResolveInfo.isEmpty() && clientResolveInfo.isEmpty();
- }
- }
-
- private record ClientResolveInfoState(
- Set schemas) {
- ClientResolveInfoState() {
- this(ConcurrentHashMap.newKeySet());
- }
- }
-
- private record FlagResolveInfo(
- ConcurrentMap variantResolveInfo,
- ConcurrentMap ruleResolveInfo) {
- FlagResolveInfo() {
- this(new ConcurrentHashMap<>(), new ConcurrentHashMap<>());
- }
-
- VariantResolveInfo variantResolveInfo(String s) {
- return variantResolveInfo.computeIfAbsent(
- s, ignore -> new VariantResolveInfo(new AtomicLong()));
- }
-
- RuleResolveInfo ruleResolveInfo(String s) {
- return ruleResolveInfo.computeIfAbsent(
- s, ignore -> new RuleResolveInfo(new AtomicLong(), new ConcurrentHashMap<>()));
- }
- }
-
- private record VariantResolveInfo(AtomicLong count) {
- void increment() {
- count.incrementAndGet();
- }
- }
-
- private record RuleResolveInfo(AtomicLong count, Map assignmentCounts) {
- void increment(String assignmentId) {
- count.incrementAndGet();
- assignmentCounts.computeIfAbsent(assignmentId, a -> new AtomicLong()).incrementAndGet();
- }
- }
-}
diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/ResolveTokenConverter.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/ResolveTokenConverter.java
deleted file mode 100644
index aa32917c..00000000
--- a/openfeature-provider-local/src/main/java/com/spotify/confidence/ResolveTokenConverter.java
+++ /dev/null
@@ -1,65 +0,0 @@
-package com.spotify.confidence;
-
-import com.google.protobuf.ByteString;
-import com.google.protobuf.Struct;
-import com.spotify.confidence.shaded.flags.resolver.v1.ResolveToken;
-import com.spotify.confidence.shaded.flags.resolver.v1.ResolveTokenV1;
-import com.spotify.confidence.shaded.flags.resolver.v1.ResolveTokenV1.AssignedFlag;
-import java.util.List;
-import java.util.Map;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-
-abstract class ResolveTokenConverter {
-
- public ByteString createResolveToken(
- String accountName,
- String resolveId,
- List resolvedValues,
- Struct evaluationContext) {
- final Map assignedFlags =
- resolvedValues.stream()
- .map(ResolveTokenConverter::toAssignedFlag)
- .collect(Collectors.toMap(AssignedFlag::getFlag, Function.identity()));
-
- final ResolveToken resolveToken =
- ResolveToken.newBuilder()
- .setTokenV1(
- ResolveTokenV1.newBuilder()
- .setAccount(accountName)
- .setResolveId(resolveId)
- .setEvaluationContext(evaluationContext)
- .putAllAssignments(assignedFlags)
- .build())
- .build();
-
- return convertResolveToken(resolveToken);
- }
-
- abstract ByteString convertResolveToken(ResolveToken resolveToken);
-
- abstract ResolveToken readResolveToken(ByteString token);
-
- static AssignedFlag toAssignedFlag(ResolvedValue value) {
- final var builder =
- AssignedFlag.newBuilder()
- .setFlag(value.flag().getName())
- .setReason(value.reason())
- .addAllFallthroughAssignments(value.fallthroughAssignments());
-
- if (value.matchedAssignment().isEmpty()) {
- return builder.build();
- }
-
- final AssignmentMatch match = value.matchedAssignment().get();
-
- return builder
- .setAssignmentId(match.assignmentId())
- .setRule(match.matchedRule().getName())
- .setSegment(match.segment().getName())
- .setVariant(match.variant().orElse(""))
- .setTargetingKey(match.targetingKey())
- .setTargetingKeySelector(match.matchedRule().getTargetingKeySelector())
- .build();
- }
-}
diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/ResolvedValue.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/ResolvedValue.java
deleted file mode 100644
index cfed5836..00000000
--- a/openfeature-provider-local/src/main/java/com/spotify/confidence/ResolvedValue.java
+++ /dev/null
@@ -1,85 +0,0 @@
-package com.spotify.confidence;
-
-import com.google.protobuf.Struct;
-import com.spotify.confidence.shaded.flags.admin.v1.Flag;
-import com.spotify.confidence.shaded.flags.admin.v1.Segment;
-import com.spotify.confidence.shaded.flags.resolver.v1.ResolveReason;
-import com.spotify.confidence.shaded.flags.resolver.v1.events.FallthroughAssignment;
-import com.spotify.confidence.shaded.flags.resolver.v1.events.FlagAssigned.DefaultAssignment.DefaultAssignmentReason;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Optional;
-
-record ResolvedValue(
- Flag flag,
- ResolveReason reason,
- Optional matchedAssignment,
- List fallthroughRules) {
- ResolvedValue(Flag flag) {
- this(flag, ResolveReason.RESOLVE_REASON_NO_SEGMENT_MATCH, Optional.empty(), List.of());
- }
-
- ResolvedValue withReason(ResolveReason reason) {
- return new ResolvedValue(flag, reason, matchedAssignment, fallthroughRules);
- }
-
- ResolvedValue withMatch(
- String assignmentId,
- String variant,
- String unit,
- Struct value,
- Segment segment,
- Flag.Rule matchedRule) {
- return new ResolvedValue(
- flag,
- ResolveReason.RESOLVE_REASON_MATCH,
- Optional.of(
- new AssignmentMatch(
- assignmentId,
- unit,
- Optional.of(variant),
- Optional.of(value),
- segment,
- matchedRule)),
- fallthroughRules);
- }
-
- ResolvedValue withClientDefaultMatch(
- String assignmentId, String unit, Segment segment, Flag.Rule matchedRule) {
- return new ResolvedValue(
- flag,
- ResolveReason.RESOLVE_REASON_MATCH,
- Optional.of(
- new AssignmentMatch(
- assignmentId, unit, Optional.empty(), Optional.empty(), segment, matchedRule)),
- fallthroughRules);
- }
-
- static DefaultAssignmentReason resolveToAssignmentReason(ResolveReason reason) {
- return switch (reason) {
- case RESOLVE_REASON_NO_SEGMENT_MATCH -> DefaultAssignmentReason.NO_SEGMENT_MATCH;
- case RESOLVE_REASON_NO_TREATMENT_MATCH -> DefaultAssignmentReason.NO_TREATMENT_MATCH;
- case RESOLVE_REASON_FLAG_ARCHIVED -> DefaultAssignmentReason.FLAG_ARCHIVED;
- default -> DefaultAssignmentReason.DEFAULT_ASSIGNMENT_REASON_UNSPECIFIED;
- };
- }
-
- ResolvedValue attributeFallthroughRule(Flag.Rule rule, String assignmentId, String unit) {
- final List attributed = new ArrayList<>(fallthroughRules);
- attributed.add(new FallthroughRule(rule, assignmentId, unit));
- return new ResolvedValue(flag, reason, matchedAssignment, attributed);
- }
-
- List fallthroughAssignments() {
- return fallthroughRules().stream()
- .map(
- assignment ->
- FallthroughAssignment.newBuilder()
- .setAssignmentId(assignment.assignmentId())
- .setRule(assignment.rule().getName())
- .setTargetingKey(assignment.targetingKey())
- .setTargetingKeySelector(assignment.rule().getTargetingKeySelector())
- .build())
- .toList();
- }
-}
diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/ResolverState.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/ResolverState.java
deleted file mode 100644
index 785964be..00000000
--- a/openfeature-provider-local/src/main/java/com/spotify/confidence/ResolverState.java
+++ /dev/null
@@ -1,103 +0,0 @@
-package com.spotify.confidence;
-
-import com.spotify.confidence.shaded.iam.v1.Client;
-import com.spotify.confidence.shaded.iam.v1.ClientCredential;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.BitSet;
-import java.util.List;
-import java.util.Map;
-import java.util.zip.GZIPOutputStream;
-
-record ResolverState(
- Map accountStates,
- Map secrets) {
-
- public com.spotify.confidence.shaded.flags.admin.v1.ResolverState toProto() {
- final com.spotify.confidence.shaded.flags.admin.v1.ResolverState.Builder builder =
- com.spotify.confidence.shaded.flags.admin.v1.ResolverState.newBuilder();
-
- // Collect all flags from all account states
- final List allFlags = new ArrayList<>();
- final List allSegments =
- new ArrayList<>();
- final List allBitsets =
- new ArrayList<>();
- final List allClients = new ArrayList<>();
- final List allClientCredentials =
- new ArrayList<>();
-
- // Process each account state
- for (AccountState accountState : accountStates.values()) {
- // Add flags
- allFlags.addAll(accountState.flags().values());
-
- // Add segments (without bitsets)
- allSegments.addAll(accountState.segments().values());
-
- // Add bitsets as packed bitsets
- for (Map.Entry entry : accountState.bitsets().entrySet()) {
- final String segmentName = entry.getKey();
- final BitSet bitSet = entry.getValue();
-
- final com.spotify.confidence.shaded.flags.admin.v1.ResolverState.PackedBitset.Builder
- bitsetBuilder =
- com.spotify.confidence.shaded.flags.admin.v1.ResolverState.PackedBitset.newBuilder()
- .setSegment(segmentName);
-
- // Check if it's a full bitset (all bits set)
- if (isFullBitset(bitSet)) {
- bitsetBuilder.setFullBitset(true);
- } else {
- // Compress the bitset
- final byte[] compressed = compressBitset(bitSet);
- bitsetBuilder.setGzippedBitset(com.google.protobuf.ByteString.copyFrom(compressed));
- }
-
- allBitsets.add(bitsetBuilder.build());
- }
- }
-
- // Process secrets to extract clients and client credentials
- for (AccountClient accountClient : secrets.values()) {
- allClients.add(accountClient.client());
- allClientCredentials.add(accountClient.clientCredential());
- }
-
- // Set all collected data
- builder.addAllFlags(allFlags);
- builder.addAllSegmentsNoBitsets(allSegments);
- builder.addAllBitsets(allBitsets);
- builder.addAllClients(allClients);
- builder.addAllClientCredentials(allClientCredentials);
-
- // Set region if available (default to UNSPECIFIED)
- builder.setRegion(
- com.spotify.confidence.shaded.flags.admin.v1.ResolverState.Region.REGION_UNSPECIFIED);
-
- return builder.build();
- }
-
- private boolean isFullBitset(BitSet bitSet) {
- // Check if all bits in the bitset are set
- final int cardinality = bitSet.cardinality();
- final int length = bitSet.length();
- return cardinality == length && length > 0;
- }
-
- private byte[] compressBitset(BitSet bitSet) {
- try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
- GZIPOutputStream gzipStream = new GZIPOutputStream(byteStream)) {
-
- // Convert BitSet to byte array
- final byte[] bytes = bitSet.toByteArray();
- gzipStream.write(bytes);
- gzipStream.finish();
-
- return byteStream.toByteArray();
- } catch (IOException e) {
- throw new RuntimeException("Failed to compress bitset", e);
- }
- }
-}
diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/SchemaFromEvaluationContext.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/SchemaFromEvaluationContext.java
deleted file mode 100644
index e1a01367..00000000
--- a/openfeature-provider-local/src/main/java/com/spotify/confidence/SchemaFromEvaluationContext.java
+++ /dev/null
@@ -1,135 +0,0 @@
-package com.spotify.confidence;
-
-import com.google.protobuf.Struct;
-import com.google.protobuf.Value;
-import com.spotify.confidence.shaded.flags.admin.v1.ContextFieldSemanticType;
-import com.spotify.confidence.shaded.flags.admin.v1.EvaluationContextSchemaField;
-import java.time.LocalDate;
-import java.time.format.DateTimeParseException;
-import java.util.HashMap;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-class SchemaFromEvaluationContext {
- private static final int MIN_DATE_LENGTH = "2025-04-01".length();
- private static final int MIN_TIMESTAMP_LENGTH = "2025-04-01T0000".length();
- private static final Set COUNTRY_CODES =
- Locale.getISOCountries(Locale.IsoCountryCode.PART1_ALPHA2);
-
- record DerivedClientSchema(
- Map fields,
- Map semanticTypes) {}
-
- static DerivedClientSchema getSchema(Struct evaluationContext) {
- final Map flatSchema = new HashMap<>();
- final Map semanticTypes = new HashMap<>();
- flattenedSchema(evaluationContext, "", flatSchema, semanticTypes);
- return new DerivedClientSchema(flatSchema, semanticTypes);
- }
-
- private static void flattenedSchema(
- Struct struct,
- String fieldPath,
- Map flatSchema,
- Map semanticTypes) {
- struct
- .getFieldsMap()
- .forEach(
- (field, value) -> {
- if (value.getKindCase() == Value.KindCase.STRUCT_VALUE) {
- flattenedSchema(
- value.getStructValue(), fieldPath + field + ".", flatSchema, semanticTypes);
- } else {
- addFieldSchema(value, fieldPath + field, flatSchema, semanticTypes);
- }
- });
- }
-
- private static void addFieldSchema(
- Value value,
- String fieldPath,
- Map flatSchema,
- Map semanticTypes) {
- final var kind = value.getKindCase();
- if (kind == Value.KindCase.STRING_VALUE) {
- flatSchema.put(fieldPath, EvaluationContextSchemaField.Kind.STRING_KIND);
- guessSemanticType(value.getStringValue(), fieldPath, semanticTypes);
- } else if (kind == Value.KindCase.BOOL_VALUE) {
- flatSchema.put(fieldPath, EvaluationContextSchemaField.Kind.BOOL_KIND);
- } else if (kind == Value.KindCase.NUMBER_VALUE) {
- flatSchema.put(fieldPath, EvaluationContextSchemaField.Kind.NUMBER_KIND);
- } else if (kind == Value.KindCase.NULL_VALUE) {
- flatSchema.put(fieldPath, EvaluationContextSchemaField.Kind.NULL_KIND);
- } else if (kind == Value.KindCase.LIST_VALUE) {
- final var subKinds =
- value.getListValue().getValuesList().stream()
- .map(Value::getKindCase)
- .collect(Collectors.toSet());
- if (subKinds.size() == 1) {
- addFieldSchema(value.getListValue().getValues(0), fieldPath, flatSchema, semanticTypes);
- }
- }
- }
-
- private static void guessSemanticType(
- String value, String fieldPath, Map semanticTypes) {
- final String lowerCasePath = fieldPath.toLowerCase(Locale.ROOT);
- if (lowerCasePath.contains("country")) {
- if (COUNTRY_CODES.contains(value.toUpperCase())) {
- semanticTypes.put(
- fieldPath,
- ContextFieldSemanticType.newBuilder()
- .setCountry(
- ContextFieldSemanticType.CountrySemanticType.newBuilder()
- .setFormat(
- ContextFieldSemanticType.CountrySemanticType.CountryFormat
- .TWO_LETTER_ISO_CODE)
- .build())
- .build());
- }
- } else if (isDate(value)) {
- semanticTypes.put(
- fieldPath,
- ContextFieldSemanticType.newBuilder()
- .setDate(ContextFieldSemanticType.DateSemanticType.getDefaultInstance())
- .build());
- } else if (isTimestamp(value)) {
- semanticTypes.put(
- fieldPath,
- ContextFieldSemanticType.newBuilder()
- .setTimestamp(ContextFieldSemanticType.TimestampSemanticType.getDefaultInstance())
- .build());
- } else if (isSemanticVersion(value)) {
- semanticTypes.put(
- fieldPath,
- ContextFieldSemanticType.newBuilder()
- .setVersion(ContextFieldSemanticType.VersionSemanticType.getDefaultInstance())
- .build());
- }
- }
-
- private static boolean isSemanticVersion(String value) {
- return SemanticVersion.isValid(value);
- }
-
- private static boolean isTimestamp(String value) {
- if (value.length() < MIN_TIMESTAMP_LENGTH) {
- return false;
- }
- return EvalUtil.parseInstant(value) != null;
- }
-
- private static boolean isDate(String value) {
- if (value.length() < MIN_DATE_LENGTH) {
- return false;
- }
- try {
- LocalDate.parse(value);
- return true;
- } catch (DateTimeParseException ex) {
- return false;
- }
- }
-}
diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/SemanticVersion.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/SemanticVersion.java
deleted file mode 100644
index 2cfa3c67..00000000
--- a/openfeature-provider-local/src/main/java/com/spotify/confidence/SemanticVersion.java
+++ /dev/null
@@ -1,119 +0,0 @@
-package com.spotify.confidence;
-
-import java.util.Objects;
-import java.util.Optional;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-class SemanticVersion implements Comparable {
-
- private static final Pattern VERSION_PATTERN =
- Pattern.compile("^(\\d{1,3})(\\.\\d{1,3})(\\.\\d{1,3})?(\\.\\d{1,10})?$");
-
- private final int major;
- private final int minor;
- private final int patch;
- private final int tag;
-
- private SemanticVersion(int major, int minor, int patch, int tag) {
- this.major = major;
- this.minor = minor;
- this.patch = patch;
- this.tag = tag;
- }
-
- /**
- * Builds a Semantic version from String
- *
- * @param version String representing semantic version
- * @return instance of Semantic version
- * @throws IllegalArgumentException in case an invalid Semver is provided
- */
- static SemanticVersion fromVersionString(final String version) {
- if (version == null || version.isEmpty()) {
- throw new IllegalArgumentException("Invalid version, version must be non-empty and not null");
- }
-
- final String[] split = version.split("-");
-
- if (split.length == 0) {
- throw new IllegalArgumentException("Invalid semantic version string: " + version);
- }
-
- final String tokenBeforeHyphens = split[0];
-
- final Matcher matcher = VERSION_PATTERN.matcher(tokenBeforeHyphens);
- if (!matcher.find()) {
- throw new IllegalArgumentException("Invalid semantic version string: " + version);
- }
-
- final int major = parseVersionSegment(matcher.group(1), -1);
- final int minor = parseVersionSegment(matcher.group(2), 0);
- final int patch = parseVersionSegment(matcher.group(3), 0);
- final int tag = parseVersionSegment(matcher.group(4), 0);
-
- if (major < 0 || minor < 0 || patch < 0 || tag < 0) {
- throw new IllegalArgumentException("Invalid semantic version string: " + version);
- }
-
- return new SemanticVersion(major, minor, patch, tag);
- }
-
- static boolean isValid(final String version) {
- try {
- fromVersionString(version);
- } catch (IllegalArgumentException ex) {
- return false;
- }
-
- return true;
- }
-
- static int parseVersionSegment(final String segment, int defaultValue) {
- return Optional.ofNullable(emptyToNull(segment))
- .map(s -> s.replace(".", ""))
- .map(Integer::parseInt)
- .orElse(defaultValue);
- }
-
- @Override
- public int compareTo(final SemanticVersion other) {
- int result = major - other.major;
- if (result == 0) {
- result = minor - other.minor;
- if (result == 0) {
- result = patch - other.patch;
- if (result == 0) {
- result = tag - other.tag;
- }
- }
- }
- return result;
- }
-
- @Override
- public String toString() {
- return String.format("%d.%d.%d.%d", major, minor, patch, tag);
- }
-
- @Override
- public boolean equals(final Object o) {
- if (this == o) {
- return true;
- }
- if (!(o instanceof SemanticVersion)) {
- return false;
- }
- final SemanticVersion that = (SemanticVersion) o;
- return major == that.major && minor == that.minor && patch == that.patch && tag == that.tag;
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(major, minor, patch, tag);
- }
-
- private static String emptyToNull(final String string) {
- return string == null || string.isEmpty() ? null : string;
- }
-}
diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/TargetingExpr.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/TargetingExpr.java
deleted file mode 100644
index 44fb893a..00000000
--- a/openfeature-provider-local/src/main/java/com/spotify/confidence/TargetingExpr.java
+++ /dev/null
@@ -1,88 +0,0 @@
-package com.spotify.confidence;
-
-import static com.spotify.confidence.Expr.T;
-import static com.spotify.confidence.Expr.and;
-import static com.spotify.confidence.Expr.not;
-import static com.spotify.confidence.Expr.or;
-import static com.spotify.confidence.Expr.ref;
-import static com.spotify.confidence.Util.evalExpression;
-import static java.util.Objects.requireNonNull;
-import static java.util.stream.Collectors.toList;
-
-import com.spotify.confidence.shaded.flags.types.v1.Expression;
-import com.spotify.confidence.shaded.flags.types.v1.Targeting;
-import com.spotify.confidence.shaded.flags.types.v1.Targeting.Criterion;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-
-class TargetingExpr {
-
- private final Expr expression;
- private final Map refs;
-
- TargetingExpr(final Expr expression, final Map refs) {
- // todo: check invariants
- // . all refs in expression exist as keys in the refs map
-
- this.expression = requireNonNull(expression);
- this.refs = requireNonNull(refs);
- }
-
- Map refs() {
- return refs;
- }
-
- boolean eval(Set trueRefs) {
- return evalExpression(expression, trueRefs);
- }
-
- @Override
- public String toString() {
- return "TargetingExpr{" + "expression=" + expression + '}';
- }
-
- static TargetingExpr fromTargeting(final Targeting targeting) {
- final Expr expr = ExprNormalizer.normalize(convert(targeting.getExpression()));
- final Set includedRefs = new HashSet<>();
- collectRefs(expr, includedRefs);
- final Map refs = new HashMap<>(targeting.getCriteriaMap());
- for (String key : Set.copyOf(refs.keySet())) {
- if (!includedRefs.contains(key)) {
- refs.remove(key);
- }
- }
-
- return new TargetingExpr(expr, refs);
- }
-
- static Expr convert(final Expression expression) {
- return switch (expression.getExpressionCase()) {
- case REF -> ref(expression.getRef());
- case NOT -> not(convert(expression.getNot()));
- case AND ->
- and(
- expression.getAnd().getOperandsList().stream()
- .map(TargetingExpr::convert)
- .collect(toList()));
- case OR ->
- or(
- expression.getOr().getOperandsList().stream()
- .map(TargetingExpr::convert)
- .collect(toList()));
- default -> T;
- };
- }
-
- private static void collectRefs(final Expr expression, final Set refs) {
- switch (expression.type()) {
- case REF -> refs.add(expression.name());
- case NOT -> collectRefs(expression.operands().iterator().next(), refs);
- case AND, OR -> {
- expression.operands().forEach(e -> collectRefs(e, refs));
- expression.operands().forEach(e -> collectRefs(e, refs));
- }
- }
- }
-}
diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/Targetings.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/Targetings.java
deleted file mode 100644
index ac3a61be..00000000
--- a/openfeature-provider-local/src/main/java/com/spotify/confidence/Targetings.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package com.spotify.confidence;
-
-import com.spotify.confidence.shaded.flags.types.v1.Expression;
-import com.spotify.confidence.shaded.flags.types.v1.Targeting;
-import java.util.Collection;
-
-class Targetings {
- static Expression or(final Collection expressions) {
- return Expression.newBuilder()
- .setOr(Expression.Operands.newBuilder().addAllOperands(expressions).build())
- .build();
- }
-
- static Targeting.Value boolValue(final boolean value) {
- return Targeting.Value.newBuilder().setBoolValue(value).build();
- }
-
- static Targeting.Value numberValue(final double value) {
- return Targeting.Value.newBuilder().setNumberValue(value).build();
- }
-
- static Targeting.Value stringValue(final String value) {
- return Targeting.Value.newBuilder().setStringValue(value).build();
- }
-
- static Targeting.Value semverValue(final String value) {
- return Targeting.Value.newBuilder()
- .setVersionValue(Targeting.SemanticVersion.newBuilder().setVersion(value).build())
- .build();
- }
-}
diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/ThreadLocalSwapWasmResolverApi.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/ThreadLocalSwapWasmResolverApi.java
index d707f1d0..862562b6 100644
--- a/openfeature-provider-local/src/main/java/com/spotify/confidence/ThreadLocalSwapWasmResolverApi.java
+++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/ThreadLocalSwapWasmResolverApi.java
@@ -109,14 +109,6 @@ public ResolveFlagsResponse resolve(ResolveFlagsRequest request) {
return getResolverForCurrentThread().resolve(request);
}
- /**
- * Returns the number of pre-initialized resolver instances. This is primarily for debugging and
- * monitoring purposes.
- */
- public int getInstanceCount() {
- return resolverInstances.size();
- }
-
/** Closes all pre-initialized resolver instances and clears the map. */
@Override
public void close() {
diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/True.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/True.java
deleted file mode 100644
index 763d0297..00000000
--- a/openfeature-provider-local/src/main/java/com/spotify/confidence/True.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package com.spotify.confidence;
-
-import java.util.Set;
-
-record True() implements Expr {
-
- @Override
- public Type type() {
- return Type.TRUE;
- }
-
- @Override
- public String name() {
- return "T";
- }
-
- @Override
- public Set operands() {
- return Set.of();
- }
-
- @Override
- public String toString() {
- return name();
- }
-}
diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/UnauthenticatedException.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/UnauthenticatedException.java
deleted file mode 100644
index 23693126..00000000
--- a/openfeature-provider-local/src/main/java/com/spotify/confidence/UnauthenticatedException.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.spotify.confidence;
-
-class UnauthenticatedException extends RuntimeException {
- UnauthenticatedException(String message) {
- super(message);
- }
-}
diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/Util.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/Util.java
deleted file mode 100644
index c80ccf58..00000000
--- a/openfeature-provider-local/src/main/java/com/spotify/confidence/Util.java
+++ /dev/null
@@ -1,377 +0,0 @@
-package com.spotify.confidence;
-
-import static com.spotify.confidence.shaded.flags.types.v1.Targeting.RangeRule.EndCase.END_EXCLUSIVE;
-import static com.spotify.confidence.shaded.flags.types.v1.Targeting.RangeRule.EndCase.END_INCLUSIVE;
-import static com.spotify.confidence.shaded.flags.types.v1.Targeting.RangeRule.EndCase.END_NOT_SET;
-import static com.spotify.confidence.shaded.flags.types.v1.Targeting.RangeRule.StartCase.START_EXCLUSIVE;
-import static com.spotify.confidence.shaded.flags.types.v1.Targeting.RangeRule.StartCase.START_INCLUSIVE;
-import static com.spotify.confidence.shaded.flags.types.v1.Targeting.RangeRule.StartCase.START_NOT_SET;
-import static java.util.stream.Collectors.toList;
-
-import com.spotify.confidence.shaded.flags.types.v1.Targeting.EqRule;
-import com.spotify.confidence.shaded.flags.types.v1.Targeting.RangeRule;
-import com.spotify.confidence.shaded.flags.types.v1.Targeting.SetRule;
-import com.spotify.confidence.shaded.flags.types.v1.Targeting.Value;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Set;
-import java.util.stream.Stream;
-
-final class Util {
-
- private Util() {}
-
- static boolean evalExpression(Expr expr, Set trueRefs) {
- return switch (expr.type()) {
- case TRUE -> true;
- case FALSE -> false;
- case REF -> trueRefs.contains(expr.name());
- case NOT -> !evalExpression(expr.operands().iterator().next(), trueRefs);
- case AND -> expr.operands().stream().allMatch(op -> evalExpression(op, trueRefs));
- case OR -> expr.operands().stream().anyMatch(op -> evalExpression(op, trueRefs));
- };
- }
-
- // eq
- static boolean hasOverlap(EqRule r1, EqRule r2) {
- ensureSameType(r1.getValue(), r2.getValue());
- final Eq eq = Eq.getEq(r1.getValue().getValueCase());
-
- return eq.eq(r1.getValue(), r2.getValue());
- }
-
- // eq, contains
- static boolean hasOverlap(EqRule r1, SetRule r2) {
- r2.getValuesList().forEach(v -> ensureSameType(r1.getValue(), v));
- final Eq eq = Eq.getEq(r1.getValue().getValueCase());
-
- return eq.contains(r2.getValuesList(), r1.getValue());
- }
-
- static boolean hasOverlap(EqRule r1, RangeRule r2) {
- return hasOverlap(r1.getValue(), r2);
- }
-
- // lt, lte
- static boolean hasOverlap(Value value, RangeRule r2) {
- ensureSameType(value, r2);
- final Ord ord = Ord.getOrd(value);
-
- final boolean afterStart;
- final boolean beforeEnd;
-
- afterStart =
- switch (r2.getStartCase()) {
- case START_INCLUSIVE -> ord.lte(r2.getStartInclusive(), value);
- case START_EXCLUSIVE -> ord.lt(r2.getStartExclusive(), value);
- default -> false;
- };
-
- beforeEnd =
- switch (r2.getEndCase()) {
- case END_INCLUSIVE -> ord.lte(value, r2.getEndInclusive());
- case END_EXCLUSIVE -> ord.lt(value, r2.getEndExclusive());
- default -> false;
- };
-
- return (r2.getStartCase() == START_NOT_SET || afterStart)
- && (r2.getEndCase() == END_NOT_SET || beforeEnd);
- }
-
- // eq, intersect
- static boolean hasOverlap(SetRule r1, SetRule r2) {
- r1.getValuesList().forEach(v1 -> r2.getValuesList().forEach(v2 -> ensureSameType(v1, v2)));
-
- final Eq eq = Eq.getEq(r1.getValuesList().get(0).getValueCase());
- return eq.overlap(r1.getValuesList(), r2.getValuesList());
- }
-
- static boolean hasOverlapNeg(SetRule r1, SetRule r2Negated) {
- r1.getValuesList()
- .forEach(v1 -> r2Negated.getValuesList().forEach(v2 -> ensureSameType(v1, v2)));
-
- final Eq eq = Eq.getEq(r1.getValuesList().get(0).getValueCase());
- return eq.overlapNeg(r1.getValuesList(), r2Negated.getValuesList());
- }
-
- static boolean hasOverlap(SetRule r1, RangeRule r2) {
- return r1.getValuesList().stream().anyMatch(value -> hasOverlap(value, r2));
- }
-
- // lt, lte
- static boolean hasOverlap(RangeRule r1, RangeRule r2) {
- ensureSameType(r1, r2);
-
- if (r1.getStartCase() == START_NOT_SET && r1.getEndCase() == END_NOT_SET) {
- return true;
- }
-
- if (r2.getStartCase() == START_NOT_SET && r2.getEndCase() == END_NOT_SET) {
- return true;
- }
-
- final boolean x1y2E = r1.getStartCase() == START_EXCLUSIVE || r2.getEndCase() == END_EXCLUSIVE;
- final boolean y1x2E = r2.getStartCase() == START_EXCLUSIVE || r1.getEndCase() == END_EXCLUSIVE;
-
- final Value x1 =
- r1.getStartCase() == START_INCLUSIVE ? r1.getStartInclusive() : r1.getStartExclusive();
- final Value x2 = r1.getEndCase() == END_INCLUSIVE ? r1.getEndInclusive() : r1.getEndExclusive();
- final Value y1 =
- r2.getStartCase() == START_INCLUSIVE ? r2.getStartInclusive() : r2.getStartExclusive();
- final Value y2 = r2.getEndCase() == END_INCLUSIVE ? r2.getEndInclusive() : r2.getEndExclusive();
-
- // .....]
- // [....]
- // or
- // [....]
- // [.....
- if (r1.getStartCase() == START_NOT_SET && r2.getStartCase() != START_NOT_SET
- || r2.getEndCase() == END_NOT_SET && r1.getEndCase() != END_NOT_SET) {
- if (y1x2E) {
- return Ord.getOrd(y1).lt(y1, x2);
- } else {
- return Ord.getOrd(y1).lte(y1, x2);
- }
- }
-
- // [.....
- // [....]
- // or
- // [....]
- // .....]
- if (r1.getEndCase() == END_NOT_SET && r2.getEndCase() != END_NOT_SET
- || r2.getStartCase() == START_NOT_SET && r1.getStartCase() != START_NOT_SET) {
- if (x1y2E) {
- return Ord.getOrd(x1).lt(x1, y2);
- } else {
- return Ord.getOrd(x1).lte(x1, y2);
- }
- }
-
- final Ord ord = Ord.getOrd(x1);
- if (x1y2E && y1x2E) {
- return ord.lt(x1, y2) && ord.lt(y1, x2);
- } else if (x1y2E) {
- return ord.lt(x1, y2) && ord.lte(y1, x2);
- } else if (y1x2E) {
- return ord.lte(x1, y2) && ord.lt(y1, x2);
- } else {
- return ord.lte(x1, y2) && ord.lte(y1, x2);
- }
- }
-
- static SetRule intersectSets(SetRule set1, SetRule set2) {
- final List values1 = set1.getValuesList();
- final List values2 = set2.getValuesList();
- values1.forEach(v1 -> values2.forEach(v2 -> ensureSameType(v1, v2)));
-
- if (values1.isEmpty() || values2.isEmpty()) {
- return SetRule.getDefaultInstance();
- }
-
- final Eq eq = Eq.getEq(values1.get(0).getValueCase());
- final List intersection =
- values1.stream().filter(v -> eq.contains(values2, v)).collect(toList());
- return SetRule.newBuilder().addAllValues(intersection).build();
- }
-
- static SetRule unionSets(SetRule set1, SetRule set2) {
- final List values1 = set1.getValuesList();
- final List values2 = set2.getValuesList();
- values1.forEach(v1 -> values2.forEach(v2 -> ensureSameType(v1, v2)));
-
- if (values1.isEmpty() && values2.isEmpty()) {
- return SetRule.getDefaultInstance();
- }
-
- if (values1.isEmpty()) {
- return set2;
- }
-
- if (values2.isEmpty()) {
- return set1;
- }
-
- final Eq eq = Eq.getEq(values1.get(0).getValueCase());
- final List filter = new ArrayList<>();
- final List union =
- Stream.concat(values1.stream(), values2.stream())
- .filter(
- v -> {
- final boolean contained = eq.contains(filter, v);
- filter.add(v);
- return !contained;
- })
- .collect(toList());
- return SetRule.newBuilder().addAllValues(union).build();
- }
-
- static SetRule subtractSet(SetRule set, SetRule subtract) {
- final List values1 = set.getValuesList();
- final List values2 = subtract.getValuesList();
- values1.forEach(v1 -> values2.forEach(v2 -> ensureSameType(v1, v2)));
-
- if (values1.isEmpty()) {
- return SetRule.getDefaultInstance();
- }
-
- final Eq eq = Eq.getEq(values1.get(0).getValueCase());
- final List subtracted =
- values1.stream().filter(v -> !eq.contains(values2, v)).collect(toList());
- return SetRule.newBuilder().addAllValues(subtracted).build();
- }
-
- static RangeRule intersectRanges(RangeRule range1, RangeRule range2) {
- ensureSameType(range1, range2);
-
- final RangeRule.Builder builder = RangeRule.newBuilder();
-
- // max of start values
- if (range1.getStartCase() != START_NOT_SET && range2.getStartCase() == START_NOT_SET) {
- if (range1.hasStartInclusive()) {
- builder.setStartInclusive(range1.getStartInclusive());
- } else if (range1.hasStartExclusive()) {
- builder.setStartExclusive(range1.getStartExclusive());
- }
- } else if (range1.getStartCase() == START_NOT_SET && range2.getStartCase() != START_NOT_SET) {
- if (range2.hasStartInclusive()) {
- builder.setStartInclusive(range2.getStartInclusive());
- } else if (range2.hasStartExclusive()) {
- builder.setStartExclusive(range2.getStartExclusive());
- }
- } else if (range1.getStartCase() != START_NOT_SET) { // both starts must be set
- final Value v1 =
- range1.hasStartInclusive() ? range1.getStartInclusive() : range1.getStartExclusive();
- final Value v2 =
- range2.hasStartInclusive() ? range2.getStartInclusive() : range2.getStartExclusive();
-
- final Ord ord = Ord.getOrd(v1);
- if (ord.lt(v1, v2)) {
- if (range2.hasStartInclusive()) {
- builder.setStartInclusive(v2);
- } else if (range2.hasStartExclusive()) {
- builder.setStartExclusive(v2);
- }
- } else if (ord.lt(v2, v1)) {
- if (range1.hasStartInclusive()) {
- builder.setStartInclusive(v1);
- } else if (range1.hasStartExclusive()) {
- builder.setStartExclusive(v1);
- }
- } else { // equal
- if (range1.hasStartExclusive() || range2.hasStartExclusive()) {
- builder.setStartExclusive(v1);
- } else {
- builder.setStartInclusive(v1);
- }
- }
- }
-
- // min of end values
- if (range1.getEndCase() != END_NOT_SET && range2.getEndCase() == END_NOT_SET) {
- if (range1.hasEndInclusive()) {
- builder.setEndInclusive(range1.getEndInclusive());
- } else if (range1.hasEndExclusive()) {
- builder.setEndExclusive(range1.getEndExclusive());
- }
- } else if (range1.getEndCase() == END_NOT_SET && range2.getEndCase() != END_NOT_SET) {
- if (range2.hasEndInclusive()) {
- builder.setEndInclusive(range2.getEndInclusive());
- } else if (range2.hasEndExclusive()) {
- builder.setEndExclusive(range2.getEndExclusive());
- }
- } else if (range1.getEndCase() != END_NOT_SET) { // both starts must be set
- final Value v1 =
- range1.hasEndInclusive() ? range1.getEndInclusive() : range1.getEndExclusive();
- final Value v2 =
- range2.hasEndInclusive() ? range2.getEndInclusive() : range2.getEndExclusive();
-
- final Ord ord = Ord.getOrd(v1);
- if (ord.lt(v1, v2)) {
- if (range1.hasEndInclusive()) {
- builder.setEndInclusive(v1);
- } else if (range1.hasEndExclusive()) {
- builder.setEndExclusive(v1);
- }
- } else if (ord.lt(v2, v1)) {
- if (range2.hasEndInclusive()) {
- builder.setEndInclusive(v2);
- } else if (range2.hasEndExclusive()) {
- builder.setEndExclusive(v2);
- }
- } else { // equal
- if (range1.hasEndExclusive() || range2.hasEndExclusive()) {
- builder.setEndExclusive(v1);
- } else {
- builder.setEndInclusive(v1);
- }
- }
- }
-
- return builder.build();
- }
-
- static SetRule filterSetByRange(SetRule set, RangeRule range) {
- final List values = set.getValuesList();
- values.forEach(v -> ensureSameType(v, range));
- // todo check all values are of same type in set
-
- final List filtered =
- values.stream().filter(v -> hasOverlap(v, range)).collect(toList());
- return SetRule.newBuilder().addAllValues(filtered).build();
- }
-
- static boolean emptyRange(RangeRule range) {
- if (range.getStartCase() != START_NOT_SET && range.getEndCase() != END_NOT_SET) {
- final Value startVal =
- range.hasStartInclusive() ? range.getStartInclusive() : range.getStartExclusive();
- final Value endVal =
- range.hasEndInclusive() ? range.getEndInclusive() : range.getEndExclusive();
-
- final Ord ord = Ord.getOrd(startVal);
- if (ord.lt(endVal, startVal)) {
- return true;
- } else if (ord.lt(startVal, endVal)) {
- return false;
- } else {
- return range.hasStartExclusive() || range.hasEndExclusive();
- }
- }
- return false;
- }
-
- private static void ensureSameType(RangeRule range1, RangeRule range2) {
- if (range1.getStartCase() == START_INCLUSIVE) {
- ensureSameType(range1.getStartInclusive(), range2);
- } else if (range1.getStartCase() == START_EXCLUSIVE) {
- ensureSameType(range1.getStartExclusive(), range2);
- }
-
- if (range1.getEndCase() == END_INCLUSIVE) {
- ensureSameType(range1.getEndInclusive(), range2);
- } else if (range1.getEndCase() == END_EXCLUSIVE) {
- ensureSameType(range1.getEndExclusive(), range2);
- }
- }
-
- private static void ensureSameType(Value value, RangeRule range) {
- if (range.getStartCase() == START_INCLUSIVE) {
- ensureSameType(value, range.getStartInclusive());
- } else if (range.getStartCase() == START_EXCLUSIVE) {
- ensureSameType(value, range.getStartExclusive());
- }
-
- if (range.getEndCase() == END_INCLUSIVE) {
- ensureSameType(value, range.getEndInclusive());
- } else if (range.getEndCase() == END_EXCLUSIVE) {
- ensureSameType(value, range.getEndExclusive());
- }
- }
-
- private static void ensureSameType(Value v1, Value v2) {
- if (v1.getValueCase() != v2.getValueCase()) {
- throw new IllegalArgumentException(
- "Non-matching types " + v1.getValueCase() + " != " + v2.getValueCase());
- }
- }
-}
diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/WasmFlagLogger.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/WasmFlagLogger.java
new file mode 100644
index 00000000..dcff465b
--- /dev/null
+++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/WasmFlagLogger.java
@@ -0,0 +1,9 @@
+package com.spotify.confidence;
+
+import com.spotify.confidence.shaded.flags.resolver.v1.WriteFlagLogsRequest;
+
+interface WasmFlagLogger {
+ void write(WriteFlagLogsRequest request);
+
+ void shutdown();
+}
diff --git a/openfeature-provider-local/src/main/java/com/spotify/confidence/WasmResolveApi.java b/openfeature-provider-local/src/main/java/com/spotify/confidence/WasmResolveApi.java
index 30b3e47a..ae72889b 100644
--- a/openfeature-provider-local/src/main/java/com/spotify/confidence/WasmResolveApi.java
+++ b/openfeature-provider-local/src/main/java/com/spotify/confidence/WasmResolveApi.java
@@ -29,14 +29,6 @@
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Function;
-class IsClosedException extends Exception {}
-
-interface WasmFlagLogger {
- void write(WriteFlagLogsRequest request);
-
- void shutdown();
-}
-
class WasmResolveApi {
private final FunctionType HOST_FN_TYPE =
FunctionType.of(List.of(ValType.I32), List.of(ValType.I32));
diff --git a/openfeature-provider-local/src/test/java/com/spotify/confidence/JavaResolveTest.java b/openfeature-provider-local/src/test/java/com/spotify/confidence/JavaResolveTest.java
deleted file mode 100644
index 1f99652d..00000000
--- a/openfeature-provider-local/src/test/java/com/spotify/confidence/JavaResolveTest.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.spotify.confidence;
-
-public class JavaResolveTest extends ResolveTest {
- public JavaResolveTest() {
- super(false);
- }
-}
diff --git a/openfeature-provider-local/src/test/java/com/spotify/confidence/ResolveTest.java b/openfeature-provider-local/src/test/java/com/spotify/confidence/ResolveTest.java
index adf41004..ce4a8de2 100644
--- a/openfeature-provider-local/src/test/java/com/spotify/confidence/ResolveTest.java
+++ b/openfeature-provider-local/src/test/java/com/spotify/confidence/ResolveTest.java
@@ -11,7 +11,6 @@
import com.spotify.confidence.shaded.flags.admin.v1.Segment;
import com.spotify.confidence.shaded.flags.resolver.v1.ResolveReason;
import com.spotify.confidence.shaded.flags.types.v1.FlagSchema;
-import java.util.BitSet;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.BeforeAll;
@@ -48,8 +47,8 @@ abstract class ResolveTest extends TestBase {
.build())
.build();
private static final String segmentA = "segments/seg-a";
- static final ResolverState exampleState;
- static final ResolverState exampleStateWithMaterialization;
+ static final byte[] exampleStateBytes;
+ static final byte[] exampleStateWithMaterializationBytes;
private static final Map flags =
Map.of(
flag1,
@@ -157,32 +156,14 @@ abstract class ResolveTest extends TestBase {
.build());
protected static final Map segments =
Map.of(segmentA, Segment.newBuilder().setName(segmentA).build());
- protected static final Map bitsets = Map.of(segmentA, getBitsetAllSet());
static {
- exampleState =
- new ResolverState(
- Map.of(
- account,
- new AccountState(
- new Account(account, Region.EU), flags, segments, bitsets, secrets, "abc")),
- secrets);
- exampleStateWithMaterialization =
- new ResolverState(
- Map.of(
- account,
- new AccountState(
- new Account(account, Region.EU),
- flagsWithMaterialization,
- segments,
- bitsets,
- secrets,
- "abc")),
- secrets);
+ exampleStateBytes = buildResolverStateBytes(flags);
+ exampleStateWithMaterializationBytes = buildResolverStateBytes(flagsWithMaterialization);
}
protected ResolveTest(boolean isWasm) {
- super(exampleState, isWasm);
+ super(exampleStateBytes);
}
@BeforeAll
@@ -198,7 +179,6 @@ public void testInvalidSecret() {
resolveWithContext(
List.of("flags/asd"),
"foo",
- "bar",
Struct.newBuilder().build(),
true,
"invalid-secret"))
@@ -208,7 +188,7 @@ public void testInvalidSecret() {
@Test
public void testInvalidFlag() {
final var response =
- resolveWithContext(List.of("flags/asd"), "foo", "bar", Struct.newBuilder().build(), false);
+ resolveWithContext(List.of("flags/asd"), "foo", Struct.newBuilder().build(), false);
assertThat(response.getResolvedFlagsList()).isEmpty();
assertThat(response.getResolveId()).isNotEmpty();
}
@@ -216,11 +196,9 @@ public void testInvalidFlag() {
@Test
public void testResolveFlag() {
final var response =
- resolveWithContext(List.of(flag1), "foo", "bar", Struct.newBuilder().build(), true);
+ resolveWithContext(List.of(flag1), "foo", Struct.newBuilder().build(), true);
assertThat(response.getResolveId()).isNotEmpty();
- final Struct expectedValue =
- // expanded with nulls to match schema
- variantOn.getValue().toBuilder().putFields("extra", Values.ofNull()).build();
+ final Struct expectedValue = variantOn.getValue();
assertEquals(variantOn.getName(), response.getResolvedFlags(0).getVariant());
assertEquals(expectedValue, response.getResolvedFlags(0).getValue());
@@ -230,11 +208,9 @@ public void testResolveFlag() {
@Test
public void testResolveFlagWithEncryptedResolveToken() {
final var response =
- resolveWithContext(List.of(flag1), "foo", "bar", Struct.newBuilder().build(), false);
+ resolveWithContext(List.of(flag1), "foo", Struct.newBuilder().build(), false);
assertThat(response.getResolveId()).isNotEmpty();
- final Struct expectedValue =
- // expanded with nulls to match schema
- variantOn.getValue().toBuilder().putFields("extra", Values.ofNull()).build();
+ final Struct expectedValue = variantOn.getValue();
assertEquals(variantOn.getName(), response.getResolvedFlags(0).getVariant());
assertEquals(expectedValue, response.getResolvedFlags(0).getValue());
@@ -274,15 +250,14 @@ public void testTooLongKey() {
.isThrownBy(
() ->
resolveWithContext(
- List.of(flag1), "a".repeat(101), "bar", Struct.newBuilder().build(), false))
+ List.of(flag1), "a".repeat(101), Struct.newBuilder().build(), false))
.withMessageContaining("Targeting key is too larger, max 100 characters.");
}
@Test
public void testResolveIntegerTargetingKeyTyped() {
final var response =
- resolveWithNumericTargetingKey(
- List.of(flag1), 1234567890, "bar", Struct.newBuilder().build(), true);
+ resolveWithNumericTargetingKey(List.of(flag1), 1234567890, Struct.newBuilder().build());
assertThat(response.getResolvedFlagsList()).hasSize(1);
assertEquals(ResolveReason.RESOLVE_REASON_MATCH, response.getResolvedFlags(0).getReason());
@@ -291,11 +266,35 @@ public void testResolveIntegerTargetingKeyTyped() {
@Test
public void testResolveDecimalUsername() {
final var response =
- resolveWithNumericTargetingKey(
- List.of(flag1), 3.14159d, "bar", Struct.newBuilder().build(), true);
+ resolveWithNumericTargetingKey(List.of(flag1), 3.14159d, Struct.newBuilder().build());
assertThat(response.getResolvedFlagsList()).hasSize(1);
assertEquals(
ResolveReason.RESOLVE_REASON_TARGETING_KEY_ERROR, response.getResolvedFlags(0).getReason());
}
+
+ private static byte[] buildResolverStateBytes(Map flagsMap) {
+ final var builder = com.spotify.confidence.shaded.flags.admin.v1.ResolverState.newBuilder();
+ builder.addAllFlags(flagsMap.values());
+ builder.addAllSegmentsNoBitsets(segments.values());
+ // All-one bitset for each segment
+ segments
+ .keySet()
+ .forEach(
+ name ->
+ builder.addBitsets(
+ com.spotify.confidence.shaded.flags.admin.v1.ResolverState.PackedBitset
+ .newBuilder()
+ .setSegment(name)
+ .setFullBitset(true)
+ .build()));
+ builder.addClients(
+ com.spotify.confidence.shaded.iam.v1.Client.newBuilder().setName(clientName).build());
+ builder.addClientCredentials(
+ com.spotify.confidence.shaded.iam.v1.ClientCredential.newBuilder()
+ .setName(credentialName)
+ .setClientSecret(TestBase.secret)
+ .build());
+ return builder.build().toByteArray();
+ }
}
diff --git a/openfeature-provider-local/src/test/java/com/spotify/confidence/TestBase.java b/openfeature-provider-local/src/test/java/com/spotify/confidence/TestBase.java
index b881dba1..66f7a093 100644
--- a/openfeature-provider-local/src/test/java/com/spotify/confidence/TestBase.java
+++ b/openfeature-provider-local/src/test/java/com/spotify/confidence/TestBase.java
@@ -10,21 +10,16 @@
import com.spotify.confidence.shaded.flags.resolver.v1.WriteFlagLogsRequest;
import com.spotify.confidence.shaded.iam.v1.Client;
import com.spotify.confidence.shaded.iam.v1.ClientCredential;
-import java.util.BitSet;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
-import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.BeforeEach;
public class TestBase {
- protected static final AtomicReference resolverState =
- new AtomicReference<>(new ResolverState(Map.of(), Map.of()));
-
protected final ResolverFallback mockFallback = mock(ResolverFallback.class);
protected static final ClientCredential.ClientSecret secret =
ClientCredential.ClientSecret.newBuilder().setSecret("very-secret").build();
- protected final ResolverState desiredState;
+ protected final byte[] desiredStateBytes;
protected static LocalResolverServiceFactory resolverServiceFactory;
static final String account = "accounts/foo";
static final String clientName = "clients/client";
@@ -40,46 +35,30 @@ public class TestBase {
.setClientSecret(secret)
.build()));
- protected TestBase(ResolverState state, boolean isWasm) {
- this.desiredState = state;
- final ResolveTokenConverter resolveTokenConverter = new PlainResolveTokenConverter();
- if (isWasm) {
- final var wasmResolverApi =
- new SwapWasmResolverApi(
- new WasmFlagLogger() {
- @Override
- public void write(WriteFlagLogsRequest request) {}
+ protected TestBase(byte[] stateBytes) {
+ this.desiredStateBytes = stateBytes;
+ final var wasmResolverApi =
+ new SwapWasmResolverApi(
+ new WasmFlagLogger() {
+ @Override
+ public void write(WriteFlagLogsRequest request) {}
- @Override
- public void shutdown() {}
- },
- desiredState.toProto().toByteArray(),
- "",
- mockFallback);
- resolverServiceFactory =
- new LocalResolverServiceFactory(
- wasmResolverApi, resolverState, resolveTokenConverter, mock(), mockFallback);
- } else {
- resolverServiceFactory =
- new LocalResolverServiceFactory(
- resolverState, resolveTokenConverter, mock(), mockFallback);
- }
+ @Override
+ public void shutdown() {}
+ },
+ desiredStateBytes,
+ "",
+ mockFallback);
+ resolverServiceFactory = new LocalResolverServiceFactory(wasmResolverApi, mockFallback);
}
protected static void setup() {}
@BeforeEach
- protected void setUp() {
- resolverState.set(desiredState);
- }
+ protected void setUp() {}
protected ResolveFlagsResponse resolveWithContext(
- List flags,
- String username,
- String structFieldName,
- Struct struct,
- boolean apply,
- String secret) {
+ List flags, String username, Struct struct, boolean apply, String secret) {
try {
return resolverServiceFactory
.create(secret)
@@ -88,8 +67,7 @@ protected ResolveFlagsResponse resolveWithContext(
.addAllFlags(flags)
.setClientSecret(secret)
.setEvaluationContext(
- Structs.of(
- "targeting_key", Values.of(username), structFieldName, Values.of(struct)))
+ Structs.of("targeting_key", Values.of(username), "bar", Values.of(struct)))
.setApply(apply)
.build())
.get();
@@ -99,32 +77,22 @@ protected ResolveFlagsResponse resolveWithContext(
}
protected ResolveFlagsResponse resolveWithNumericTargetingKey(
- List flags,
- Number targetingKey,
- String structFieldName,
- Struct struct,
- boolean apply) {
+ List flags, Number targetingKey, Struct struct) {
try {
final var builder =
ResolveFlagsRequest.newBuilder()
.addAllFlags(flags)
.setClientSecret(secret.getSecret())
- .setApply(apply);
+ .setApply(true);
if (targetingKey instanceof Double || targetingKey instanceof Float) {
builder.setEvaluationContext(
Structs.of(
- "targeting_key",
- Values.of(targetingKey.doubleValue()),
- structFieldName,
- Values.of(struct)));
+ "targeting_key", Values.of(targetingKey.doubleValue()), "bar", Values.of(struct)));
} else {
builder.setEvaluationContext(
Structs.of(
- "targeting_key",
- Values.of(targetingKey.longValue()),
- structFieldName,
- Values.of(struct)));
+ "targeting_key", Values.of(targetingKey.longValue()), "bar", Values.of(struct)));
}
return resolverServiceFactory.create(secret.getSecret()).resolveFlags(builder.build()).get();
@@ -134,14 +102,7 @@ protected ResolveFlagsResponse resolveWithNumericTargetingKey(
}
protected ResolveFlagsResponse resolveWithContext(
- List flags, String username, String structFieldName, Struct struct, boolean apply) {
- return resolveWithContext(flags, username, structFieldName, struct, apply, secret.getSecret());
- }
-
- protected static BitSet getBitsetAllSet() {
- final BitSet bitset = new BitSet(1000000);
- bitset.flip(0, bitset.size());
-
- return bitset;
+ List flags, String username, Struct struct, boolean apply) {
+ return resolveWithContext(flags, username, struct, apply, secret.getSecret());
}
}
diff --git a/openfeature-provider-local/src/test/java/com/spotify/confidence/WasmResolveTest.java b/openfeature-provider-local/src/test/java/com/spotify/confidence/WasmResolveTest.java
index 78a6b622..4783caca 100644
--- a/openfeature-provider-local/src/test/java/com/spotify/confidence/WasmResolveTest.java
+++ b/openfeature-provider-local/src/test/java/com/spotify/confidence/WasmResolveTest.java
@@ -30,8 +30,8 @@
import org.junit.jupiter.api.Test;
public class WasmResolveTest extends ResolveTest {
- // Override desiredState to use materialization-enabled state for StickyResolveStrategy tests
- protected final ResolverState desiredState = ResolveTest.exampleStateWithMaterialization;
+ // Use materialization-enabled state bytes for StickyResolveStrategy tests
+ protected final byte[] desiredStateBytes = ResolveTest.exampleStateWithMaterializationBytes;
public WasmResolveTest() {
super(true);
@@ -39,8 +39,7 @@ public WasmResolveTest() {
@Test
public void testAccountStateProviderInterface() {
- final AccountStateProvider customProvider =
- () -> ResolveTest.exampleState.toProto().toByteArray();
+ final AccountStateProvider customProvider = () -> ResolveTest.exampleStateBytes;
final OpenFeatureLocalResolveProvider localResolveProvider =
new OpenFeatureLocalResolveProvider(
customProvider,
@@ -66,7 +65,6 @@ public void close() {}
assertTrue(objectEvaluation.getValue().isStructure());
final var structure = objectEvaluation.getValue().asStructure();
assertEquals("on", structure.getValue("data").asString());
- assertTrue(structure.getValue("extra").isNull());
}
@Test
@@ -101,7 +99,7 @@ public void testResolverFallbackWhenMaterializationsMissing() {
// Create the provider with mocked fallback strategy
final OpenFeatureLocalResolveProvider provider =
new OpenFeatureLocalResolveProvider(
- () -> desiredState.toProto().toByteArray(), "", secret.getSecret(), mockFallback);
+ () -> desiredStateBytes, "", secret.getSecret(), mockFallback);
// Make the resolve request using OpenFeature API
final ProviderEvaluation evaluation =
@@ -135,7 +133,7 @@ public void testMaterializationRepositoryWhenMaterializationsMissing() {
// Create the provider with mocked repository strategy
final OpenFeatureLocalResolveProvider provider =
new OpenFeatureLocalResolveProvider(
- () -> desiredState.toProto().toByteArray(), "", secret.getSecret(), mockRepository);
+ () -> desiredStateBytes, "", secret.getSecret(), mockRepository);
// Make the resolve request using OpenFeature API
final ProviderEvaluation evaluation =
@@ -163,7 +161,7 @@ public void testConcurrentResolveLoadTest() throws InterruptedException {
// Create the provider using normal exampleState (not with materialization)
final OpenFeatureLocalResolveProvider provider =
new OpenFeatureLocalResolveProvider(
- () -> ResolveTest.exampleState.toProto().toByteArray(),
+ () -> ResolveTest.exampleStateBytes,
"",
TestBase.secret.getSecret(),
new ResolverFallback() {
diff --git a/pom.xml b/pom.xml
index ad39bea3..d6cba637 100644
--- a/pom.xml
+++ b/pom.xml
@@ -198,11 +198,6 @@
gson
2.11.0