From 8aeeb80b92ef8a01d3a9365171b8b5b96d4d2935 Mon Sep 17 00:00:00 2001 From: Fahad Heylaal Date: Thu, 23 Jul 2026 23:08:30 +0200 Subject: [PATCH 1/3] feat: v3 alignments --- .github/workflows/checks.yml | 36 +- .github/workflows/publish.yml | 30 +- README.md | 29 +- conformance/sdk-v3.json | 175 ++++++++- .../FeaturevisorOpenFeatureProvider.java | 17 +- .../main/java/com/featurevisor/cli/CLI.java | 21 +- .../java/com/featurevisor/sdk/Bucketer.java | 58 ++- .../com/featurevisor/sdk/ChildInstance.java | 85 ++++- .../com/featurevisor/sdk/CompareVersions.java | 11 +- .../java/com/featurevisor/sdk/Conditions.java | 96 ++--- .../featurevisor/sdk/ConfigureBucketKey.java | 6 + .../sdk/ConfigureBucketKeyOptions.java | 31 ++ .../sdk/ConfigureBucketValue.java | 6 + .../sdk/ConfigureBucketValueOptions.java | 31 ++ .../com/featurevisor/sdk/ContextUtils.java | 2 +- .../{Logger.java => DiagnosticReporter.java} | 74 ++-- .../java/com/featurevisor/sdk/Emitter.java | 144 ++------ .../java/com/featurevisor/sdk/Evaluate.java | 34 +- .../featurevisor/sdk/EvaluateByBucketing.java | 56 +-- .../featurevisor/sdk/EvaluateDisabled.java | 10 +- .../com/featurevisor/sdk/EvaluateForced.java | 20 +- .../featurevisor/sdk/EvaluateNotFound.java | 19 +- .../com/featurevisor/sdk/EvaluateOptions.java | 47 +-- .../com/featurevisor/sdk/EvaluateSticky.java | 10 +- .../java/com/featurevisor/sdk/Events.java | 10 +- .../com/featurevisor/sdk/Featurevisor.java | 129 ++++--- .../sdk/FeaturevisorEventDetails.java | 14 + .../sdk/FeaturevisorEventHandler.java | 6 + .../sdk/FeaturevisorEventName.java | 27 ++ .../featurevisor/sdk/FeaturevisorModule.java | 16 +- .../sdk/FeaturevisorUnsubscribe.java | 6 + ...va => InstanceEvaluationDataProvider.java} | 59 +-- .../com/featurevisor/sdk/ModulesManager.java | 63 +--- .../java/com/featurevisor/sdk/MurmurHash.java | 2 +- .../java/com/featurevisor/sdk/Operator.java | 15 + .../com/featurevisor/sdk/BucketerTest.java | 54 ++- .../java/com/featurevisor/sdk/ChildTest.java | 46 ++- .../com/featurevisor/sdk/ConditionsTest.java | 335 +++++++++++------- ...rTest.java => DiagnosticReporterTest.java} | 178 +++++----- .../com/featurevisor/sdk/EmitterTest.java | 88 ++--- .../sdk/EvaluateDisabledTest.java | 62 ++-- .../featurevisor/sdk/EvaluateForcedTest.java | 50 +-- .../sdk/EvaluateNotFoundTest.java | 56 +-- .../featurevisor/sdk/EvaluateStickyTest.java | 38 +- .../java/com/featurevisor/sdk/EventsTest.java | 16 +- .../featurevisor/sdk/FeaturevisorTest.java | 39 +- ...luationDataProviderComprehensiveTest.java} | 106 +++--- ...> InstanceEvaluationDataProviderTest.java} | 134 ++++++- .../featurevisor/sdk/ModulesManagerTest.java | 8 +- .../com/featurevisor/sdk/PublicApiTest.java | 27 ++ pom.xml | 2 +- 51 files changed, 1642 insertions(+), 992 deletions(-) create mode 100644 featurevisor-sdk/src/main/java/com/featurevisor/sdk/ConfigureBucketKey.java create mode 100644 featurevisor-sdk/src/main/java/com/featurevisor/sdk/ConfigureBucketKeyOptions.java create mode 100644 featurevisor-sdk/src/main/java/com/featurevisor/sdk/ConfigureBucketValue.java create mode 100644 featurevisor-sdk/src/main/java/com/featurevisor/sdk/ConfigureBucketValueOptions.java rename featurevisor-sdk/src/main/java/com/featurevisor/sdk/{Logger.java => DiagnosticReporter.java} (62%) create mode 100644 featurevisor-sdk/src/main/java/com/featurevisor/sdk/FeaturevisorEventDetails.java create mode 100644 featurevisor-sdk/src/main/java/com/featurevisor/sdk/FeaturevisorEventHandler.java create mode 100644 featurevisor-sdk/src/main/java/com/featurevisor/sdk/FeaturevisorEventName.java create mode 100644 featurevisor-sdk/src/main/java/com/featurevisor/sdk/FeaturevisorUnsubscribe.java rename featurevisor-sdk/src/main/java/com/featurevisor/sdk/{DatafileReader.java => InstanceEvaluationDataProvider.java} (89%) rename featurevisor-sdk/src/test/java/com/featurevisor/sdk/{LoggerTest.java => DiagnosticReporterTest.java} (59%) rename featurevisor-sdk/src/test/java/com/featurevisor/sdk/{DatafileReaderComprehensiveTest.java => InstanceEvaluationDataProviderComprehensiveTest.java} (74%) rename featurevisor-sdk/src/test/java/com/featurevisor/sdk/{DatafileReaderTest.java => InstanceEvaluationDataProviderTest.java} (71%) create mode 100644 featurevisor-sdk/src/test/java/com/featurevisor/sdk/PublicApiTest.java diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 6ea0b2a..d5bead3 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -14,27 +14,55 @@ concurrency: cancel-in-progress: true jobs: - checks: + java: + name: Java ${{ matrix.java }} runs-on: ubuntu-latest timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + include: + - java: "11" + goal: "-Dmaven.test.skip=true clean package" + - java: "25" + goal: "clean verify" steps: - name: Check out repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up Java uses: actions/setup-java@v5 with: - java-version: "24" + java-version: ${{ matrix.java }} distribution: temurin cache: maven - name: Build and test Maven reactor - run: mvn --batch-mode clean verify + run: mvn --batch-mode ${{ matrix.goal }} - name: Verify published artifact boundaries run: bash scripts/verify-artifacts.sh + example: + name: Featurevisor example-1 + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Check out repository + uses: actions/checkout@v7 + + - name: Set up Java + uses: actions/setup-java@v5 + with: + java-version: "25" + distribution: temurin + cache: maven + + - name: Build SDK + run: mvn --batch-mode clean install + - name: Set up Node.js uses: actions/setup-node@v7 with: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f93e356..f37e2c1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -20,12 +20,12 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up Java and Maven publishing uses: actions/setup-java@v5 with: - java-version: "24" + java-version: "25" distribution: temurin cache: maven server-id: github @@ -36,14 +36,36 @@ jobs: id: version shell: bash run: | + if [[ ! "$GITHUB_REF_NAME" =~ ^v2\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then + echo "Expected a Featurevisor Java v2 semantic version tag" >&2 + exit 1 + fi version="${GITHUB_REF_NAME#v}" + if ! grep -Fq "$version" pom.xml; then + echo "Tag $GITHUB_REF_NAME does not match the Maven revision" >&2 + exit 1 + fi + if ! grep -Fq "$version" README.md; then + echo "README installation version does not match $GITHUB_REF_NAME" >&2 + exit 1 + fi echo "version=$version" >> "$GITHUB_OUTPUT" echo "Publishing Featurevisor Java $version" - - name: Build, test, and publish all artifacts + - name: Build and test all artifacts + run: >- + mvn --batch-mode + -Drevision=${{ steps.version.outputs.version }} + clean verify + + - name: Verify published artifact boundaries + run: bash scripts/verify-artifacts.sh + + - name: Publish all artifacts env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: >- mvn --batch-mode -Drevision=${{ steps.version.outputs.version }} - clean deploy + -DskipTests + deploy diff --git a/README.md b/README.md index 79fb3b4..bb30299 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ Add the Featurevisor Java SDK as a dependency with your desired version: com.featurevisor featurevisor-java - 0.1.0 + 2.0.0 ``` @@ -133,6 +133,8 @@ Featurevisor f = Featurevisor.createFeaturevisor( Most applications only need `Featurevisor.createFeaturevisor`, the `Featurevisor` instance type, and `Featurevisor.FeaturevisorOptions`. Public extension and observability types include `FeaturevisorModule`, `FeaturevisorDiagnostic`, and the datafile model types. +Concurrent evaluations are safe after an instance is configured. Do not call state-changing methods such as `setDatafile`, `setContext`, `setSticky`, `addModule`, `removeModule`, or `close` concurrently with evaluations or with each other. Apply those changes from a serialized update path. Module, event, and diagnostic callbacks must synchronize mutable state that they capture. + ## Initialization The SDK can be initialized by passing [datafile](https://featurevisor.com/docs/building-datafiles/) content directly: @@ -572,7 +574,7 @@ You can listen to these events that can occur at various stages in your applicat ### `datafile_set` ```java -Runnable unsubscribe = f.on("datafile_set", (event) -> { +FeaturevisorUnsubscribe unsubscribe = f.on(FeaturevisorEventName.DATAFILE_SET, (event) -> { String revision = (String) event.get("revision"); // new revision String previousRevision = (String) event.get("previousRevision"); Boolean revisionChanged = (Boolean) event.get("revisionChanged"); // true if revision has changed @@ -586,7 +588,7 @@ Runnable unsubscribe = f.on("datafile_set", (event) -> { }); // stop listening to the event -unsubscribe.run(); +unsubscribe.unsubscribe(); ``` The `features` array will contain keys of features that have either been: @@ -600,7 +602,7 @@ compared to the previous datafile content that existed in the SDK instance. ### `context_set` ```java -Runnable unsubscribe = f.on("context_set", (event) -> { +FeaturevisorUnsubscribe unsubscribe = f.on(FeaturevisorEventName.CONTEXT_SET, (event) -> { Boolean replaced = (Boolean) event.get("replaced"); // true if context was replaced @SuppressWarnings("unchecked") Map context = (Map) event.get("context"); // the new context @@ -612,7 +614,7 @@ Runnable unsubscribe = f.on("context_set", (event) -> { ### `sticky_set` ```java -Runnable unsubscribe = f.on("sticky_set", (event) -> { +FeaturevisorUnsubscribe unsubscribe = f.on(FeaturevisorEventName.STICKY_SET, (event) -> { Boolean replaced = (Boolean) event.get("replaced"); // true if sticky features got replaced @SuppressWarnings("unchecked") List features = (List) event.get("features"); // list of all affected feature keys @@ -624,7 +626,7 @@ Runnable unsubscribe = f.on("sticky_set", (event) -> { ### `error` ```java -Emitter.UnsubscribeFunction unsubscribe = f.on(Emitter.EventName.ERROR, (event) -> { +FeaturevisorUnsubscribe unsubscribe = f.on(FeaturevisorEventName.ERROR, (event) -> { FeaturevisorDiagnostic diagnostic = (FeaturevisorDiagnostic) event.get("diagnostic"); System.err.println(diagnostic.getMessage()); }); @@ -638,16 +640,16 @@ Besides logging with debug level enabled, you can also get more details about ho ```java // flag -Map evaluation = f.evaluateFlag(featureKey, context); +Evaluation evaluation = f.evaluateFlag(featureKey, context); // variation -Map evaluation = f.evaluateVariation(featureKey, context); +Evaluation evaluation = f.evaluateVariation(featureKey, context); // variable -Map evaluation = f.evaluateVariable(featureKey, variableKey, context); +Evaluation evaluation = f.evaluateVariable(featureKey, variableKey, context); ``` -The returned object will always contain the following properties: +The returned `Evaluation` exposes the following properties: - `featureKey`: the feature key - `reason`: the reason how the value was evaluated @@ -749,6 +751,8 @@ FeaturevisorModule module = new FeaturevisorModule("diagnostic-module") ## Child instance +A child snapshots the parent keys that exist when it is spawned. Child values win for those keys. Parent keys introduced later are still inherited. Calling `close()` removes both child-owned listeners and subscriptions delegated to the parent. + When dealing with purely client-side applications, it is understandable that there is only one user involved, like in browser or mobile applications. But when using Featurevisor SDK in server-side applications, where a single server instance can handle multiple user requests simultaneously, it is important to isolate the context for each request. @@ -774,8 +778,11 @@ Similar to parent SDK, child instances also support several additional methods: - `setContext` - `setSticky` +- `evaluateFlag` - `isEnabled` +- `evaluateVariation` - `getVariation` +- `evaluateVariable` - `getVariable` - `getVariableBoolean` - `getVariableString` @@ -917,7 +924,7 @@ $ make verify-artifacts ### Releasing - Manually create a new release on [GitHub](https://github.com/featurevisor/featurevisor-java/releases) -- Tag it with a prefix of `v`, like `v1.0.0` +- Tag it with a prefix of `v`, like `v2.0.0` - GitHub Actions publishes the parent POM, Java SDK, and OpenFeature provider to [GitHub Packages](https://github.com/orgs/featurevisor/packages?repo_name=featurevisor-java) ## License diff --git a/conformance/sdk-v3.json b/conformance/sdk-v3.json index ceae692..49396ce 100644 --- a/conformance/sdk-v3.json +++ b/conformance/sdk-v3.json @@ -1,5 +1,5 @@ { - "version": 1, + "version": 2, "description": "Featurevisor v3 cross SDK compatibility contracts", "bucketing": { "minimum": 0, @@ -26,7 +26,47 @@ "pattern": "chrome", "flags": "g", "values": ["chrome", "chrome", "firefox", "chrome"], - "matches": [true, true, false, true] + "matches": [true, true, false, true], + "portableCases": [ + { + "pattern": "^chrome$", + "flags": "", + "value": "chrome", + "expected": true + }, + { + "pattern": "^(chrome|firefox)$", + "flags": "i", + "value": "Firefox", + "expected": true + }, + { + "pattern": "^second$", + "flags": "m", + "value": "first\nsecond", + "expected": true + }, + { + "pattern": "first.*second", + "flags": "s", + "value": "first\nsecond", + "expected": true + }, + { + "pattern": "\\(literal\\)", + "flags": "g", + "value": "(literal)", + "expected": true + } + ], + "rejectedSyntax": [ + "foo(?=bar)", + "(?<=foo)bar", + "(?:foo|bar)", + "(?foo)", + "(foo)\\1", + "foo++" + ] }, "typedVariables": [ { "type": "integer", "value": 1, "valid": true }, @@ -44,6 +84,135 @@ "diagnostics": { "requiredFields": ["level", "code", "message", "details"], "detailsType": "object", - "emptyDetailsJson": "{}" + "emptyDetailsJson": "{}", + "evaluationDetailFields": ["featureKey", "variableKey", "reason", "evaluation"], + "moduleEnvelopeFields": ["module", "moduleName", "originalError"], + "errorEventLevels": ["error"] + }, + "numericBucketKeys": [ + { "value": 1.2345678901234567, "expected": "1.2345678901234567" }, + { "value": 0.30000000000000004, "expected": "0.30000000000000004" }, + { "value": 0.000001, "expected": "0.000001" }, + { "value": 1e-7, "expected": "1e-7" }, + { "value": 100000000000000000000, "expected": "100000000000000000000" }, + { "value": 1e21, "expected": "1e+21" } + ], + "portableConditions": { + "regexFlags": ["g", "i", "m", "s"], + "rejectedRegexFlags": ["d", "u", "v", "y"], + "dateFormat": "ISO 8601 with an explicit timezone", + "dates": [ + "2024-01-01T00:00:00Z", + "2024-01-01T01:00:00+01:00", + "2024-01-01T00:00:00.250Z", + "2024-01-01T01:00:00.250+01:00" + ], + "semanticVersions": [ + "1.2.3", + "1.2.3-beta.1", + "1.2.3+build.5" + ], + "invalidSemanticVersion": "invalid", + "invalidSemanticVersionDiagnosticCode": "condition_match_error" + }, + "conditionCases": [ + { + "name": "strict primitive equality", + "condition": { + "attribute": "value", + "operator": "equals", + "value": 1 + }, + "context": { "value": "1" }, + "expected": false + }, + { + "name": "not negates implicit and", + "condition": { + "not": [ + { "attribute": "country", "operator": "equals", "value": "us" }, + { "attribute": "device", "operator": "equals", "value": "mobile" } + ] + }, + "context": { "country": "us", "device": "desktop" }, + "expected": true + }, + { + "name": "not with nested or means none match", + "condition": { + "not": [ + { + "or": [ + { "attribute": "country", "operator": "equals", "value": "us" }, + { "attribute": "country", "operator": "equals", "value": "nl" } + ] + } + ] + }, + "context": { "country": "de" }, + "expected": true + }, + { + "name": "empty not fails defensively", + "condition": { "not": [] }, + "context": {}, + "expected": false + }, + { + "name": "fractional ISO date with offset", + "condition": { + "attribute": "date", + "operator": "before", + "value": "2024-01-01T00:00:00.500Z" + }, + "context": { "date": "2024-01-01T01:00:00.250+01:00" }, + "expected": true + } + ], + "childInstances": { + "contextModel": "snapshot existing parent keys at spawn, inherit newly introduced parent keys, child keys win", + "closeRemovesLocalAndDelegatedSubscriptions": true, + "detailedEvaluationMethods": ["flag", "variation", "variable"], + "contextCase": { + "parentAtSpawn": { "country": "nl", "plan": "free" }, + "child": { "country": "de" }, + "parentAfterSpawn": { "country": "us", "plan": "pro", "region": "eu" }, + "expected": { "country": "de", "plan": "free", "region": "eu" } + } + }, + "defaults": { + "presenceBased": true, + "values": ["", 0, false, null], + "aggregateEvaluationPreservesEmptyVariation": true, + "aggregateCase": { + "datafile": { + "schemaVersion": "2", + "revision": "defaults", + "segments": {}, + "features": { + "experiment": { + "key": "experiment", + "bucketBy": "userId", + "variations": [{ "value": "control" }], + "traffic": [] + } + } + }, + "defaultVariationValue": "", + "expected": { + "enabled": false, + "variation": "" + } + } + }, + "diagnosticCase": { + "featureKey": "missing", + "expectedLevel": "warn", + "expectedCode": "feature_not_found", + "detailsMustBeObject": true + }, + "nativeContexts": { + "numericTypesUseOneComparisonContract": true, + "primitiveNativeSlicesSupportIncludes": true } } diff --git a/featurevisor-openfeature/src/main/java/com/featurevisor/openfeature/FeaturevisorOpenFeatureProvider.java b/featurevisor-openfeature/src/main/java/com/featurevisor/openfeature/FeaturevisorOpenFeatureProvider.java index c282e37..cf211e0 100644 --- a/featurevisor-openfeature/src/main/java/com/featurevisor/openfeature/FeaturevisorOpenFeatureProvider.java +++ b/featurevisor-openfeature/src/main/java/com/featurevisor/openfeature/FeaturevisorOpenFeatureProvider.java @@ -2,9 +2,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.featurevisor.sdk.Evaluation; -import com.featurevisor.sdk.Emitter; import com.featurevisor.sdk.Featurevisor; import com.featurevisor.sdk.FeaturevisorDiagnosticHandler; +import com.featurevisor.sdk.FeaturevisorEventName; +import com.featurevisor.sdk.FeaturevisorUnsubscribe; import com.featurevisor.sdk.VariableType; import dev.openfeature.sdk.ErrorCode; import dev.openfeature.sdk.EvaluationContext; @@ -52,7 +53,7 @@ public static final class Options { private final String keySeparator; private final String variationKey; private final TrackingHandler onTrack; - private final Emitter.UnsubscribeFunction datafileUnsubscribe; + private final FeaturevisorUnsubscribe datafileUnsubscribe; private final boolean ownsFeaturevisor; private String datafileError; @@ -83,7 +84,7 @@ public FeaturevisorOpenFeatureProvider(Options options) { }); this.featurevisor = Featurevisor.createFeaturevisor(fvOptions); } - this.datafileUnsubscribe = this.featurevisor.on(Emitter.EventName.DATAFILE_SET, details -> datafileError = null); + this.datafileUnsubscribe = this.featurevisor.on(FeaturevisorEventName.DATAFILE_SET, details -> datafileError = null); } public FeaturevisorOpenFeatureProvider(Featurevisor.FeaturevisorOptions options) { @@ -221,20 +222,22 @@ private static boolean matches(Object value, String expected) { return value instanceof Map || value instanceof List; } - private static Map normalizeMap(Map input) { + private static Map normalizeMap(Map input) { Map result = new HashMap<>(); - input.forEach((key, value) -> result.put(key, normalize(value))); + input.forEach((key, value) -> { + if (key instanceof String) result.put((String) key, normalize(value)); + }); return result; } private static Object normalize(Object value) { if (value instanceof Instant) return value.toString(); - if (value instanceof Map) return normalizeMap((Map) value); + if (value instanceof Map) return normalizeMap((Map) value); if (value instanceof List) { List result = new ArrayList<>(); for (Object item : (List) value) result.add(normalize(item)); return result; } return value; } private static Value toValue(Object value) throws InstantiationException { if (value instanceof Value) return (Value) value; - if (value instanceof Map) return new Value(Structure.mapToStructure((Map) value)); + if (value instanceof Map) return new Value(Structure.mapToStructure(normalizeMap((Map) value))); if (value instanceof List) { List result = new ArrayList<>(); for (Object item : (List) value) result.add(toValue(item)); return new Value(result); } return new Value(value); } diff --git a/featurevisor-sdk/src/main/java/com/featurevisor/cli/CLI.java b/featurevisor-sdk/src/main/java/com/featurevisor/cli/CLI.java index 33d196c..b13c6d2 100644 --- a/featurevisor-sdk/src/main/java/com/featurevisor/cli/CLI.java +++ b/featurevisor-sdk/src/main/java/com/featurevisor/cli/CLI.java @@ -33,7 +33,7 @@ @Command( name = "featurevisor", mixinStandardHelpOptions = true, - version = "1.0.0", + version = "2.0.0", description = "Featurevisor Java Library CLI - Test runner, benchmark, and distribution assessment" ) public class CLI implements Runnable { @@ -317,9 +317,9 @@ private List getTargets(String featurevisorProjectPath) throws IOExcepti } /** - * Get logger level based on CLI options + * Get diagnostics level based on CLI options */ - private FeaturevisorLogLevel getLoggerLevel() { + private FeaturevisorLogLevel getDiagnosticsLevel() { if (verbose) { return FeaturevisorLogLevel.DEBUG; } else if (quiet) { @@ -569,8 +569,7 @@ private com.featurevisor.sdk.Evaluation evaluateFlag(Object f, String featureKey if (f instanceof Featurevisor) { return ((Featurevisor) f).evaluateFlag(featureKey, context); } else if (f instanceof com.featurevisor.sdk.ChildInstance) { - // ChildInstance doesn't have evaluateFlag, so we'll skip this test for child instances - return null; + return ((com.featurevisor.sdk.ChildInstance) f).evaluateFlag(featureKey, context); } return null; } @@ -579,8 +578,7 @@ private com.featurevisor.sdk.Evaluation evaluateVariation(Object f, String featu if (f instanceof Featurevisor) { return ((Featurevisor) f).evaluateVariation(featureKey, context, options); } else if (f instanceof com.featurevisor.sdk.ChildInstance) { - // ChildInstance doesn't have evaluateVariation, so we'll skip this test for child instances - return null; + return ((com.featurevisor.sdk.ChildInstance) f).evaluateVariation(featureKey, context, options); } return null; } @@ -589,8 +587,7 @@ private com.featurevisor.sdk.Evaluation evaluateVariable(Object f, String featur if (f instanceof Featurevisor) { return ((Featurevisor) f).evaluateVariable(featureKey, variableKey, context, options); } else if (f instanceof com.featurevisor.sdk.ChildInstance) { - // ChildInstance doesn't have evaluateVariable, so we'll skip this test for child instances - return null; + return ((com.featurevisor.sdk.ChildInstance) f).evaluateVariable(featureKey, variableKey, context, options); } return null; } @@ -679,7 +676,7 @@ private void test() { System.out.println(); - FeaturevisorLogLevel level = getLoggerLevel(); + FeaturevisorLogLevel level = getDiagnosticsLevel(); List> tests = getTests(featurevisorProjectPath); if (tests.isEmpty()) { @@ -848,7 +845,7 @@ private void benchmark() { contextMap = objectMapper.readValue(context, new TypeReference>() {}); } - FeaturevisorLogLevel level = getLoggerLevel(); + FeaturevisorLogLevel level = getDiagnosticsLevel(); String target = targets.isEmpty() ? null : targets.get(0); DatafileContent datafile = buildDatafile(rootDirectoryPath, environment, target); @@ -948,7 +945,7 @@ private void assessDistribution() { Featurevisor f = Featurevisor.createFeaturevisor(new Featurevisor.FeaturevisorOptions() .datafile(datafile) - .logLevel(getLoggerLevel())); + .logLevel(getDiagnosticsLevel())); Object value = null; diff --git a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/Bucketer.java b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/Bucketer.java index 7a68ab7..0541faf 100644 --- a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/Bucketer.java +++ b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/Bucketer.java @@ -4,6 +4,7 @@ import java.util.Map; import java.util.List; import java.util.ArrayList; +import java.math.BigDecimal; /** * Bucketer for Featurevisor SDK @@ -58,7 +59,7 @@ public static class GetBucketKeyOptions { private String featureKey; private Bucket bucketBy; private Map context; - private Logger logger; + private DiagnosticReporter diagnostics; public GetBucketKeyOptions() {} @@ -77,8 +78,8 @@ public GetBucketKeyOptions context(Map context) { return this; } - public GetBucketKeyOptions logger(Logger logger) { - this.logger = logger; + public GetBucketKeyOptions diagnostics(DiagnosticReporter diagnostics) { + this.diagnostics = diagnostics; return this; } @@ -86,19 +87,19 @@ public GetBucketKeyOptions logger(Logger logger) { public String getFeatureKey() { return featureKey; } public Bucket getBucketBy() { return bucketBy; } public Map getContext() { return context; } - public Logger getLogger() { return logger; } + public DiagnosticReporter getDiagnostics() { return diagnostics; } } /** * Get a bucket key from the given options - * @param options The options containing feature key, bucketBy, context, and logger + * @param options The options containing feature key, bucketBy, context, and diagnostics * @return The bucket key string */ public static String getBucketKey(GetBucketKeyOptions options) { String featureKey = options.getFeatureKey(); Bucket bucketBy = options.getBucketBy(); Map context = options.getContext(); - Logger logger = options.getLogger(); + DiagnosticReporter diagnostics = options.getDiagnostics(); String type; List attributeKeys; @@ -117,7 +118,7 @@ public static String getBucketKey(GetBucketKeyOptions options) { Map details = new java.util.HashMap<>(); details.put("featureKey", featureKey); details.put("bucketBy", bucketBy); - logger.error("invalid bucketBy", details); + diagnostics.error("invalid bucketBy", details); throw new RuntimeException("invalid bucketBy"); } @@ -127,7 +128,7 @@ public static String getBucketKey(GetBucketKeyOptions options) { for (String attributeKey : attributeKeys) { Object attributeValue = ContextUtils.getValueFromContext(context, attributeKey); - if (attributeValue == null) { + if (attributeValue == null && !pathExists(context, attributeKey)) { continue; } @@ -144,6 +145,45 @@ public static String getBucketKey(GetBucketKeyOptions options) { bucketKey.add(featureKey); return String.join(DEFAULT_BUCKET_KEY_SEPARATOR, - bucketKey.stream().map(Object::toString).toArray(String[]::new)); + bucketKey.stream().map(Bucketer::javascriptString).toArray(String[]::new)); + } + + @SuppressWarnings("unchecked") + private static boolean pathExists(Map context, String path) { + Object current = context; + for (String key : path.split("\\.")) { + if (!(current instanceof Map) || !((Map) current).containsKey(key)) { + return false; + } + current = ((Map) current).get(key); + } + return true; + } + + private static String javascriptString(Object value) { + if (value == null) return ""; + if (value instanceof Boolean) return (Boolean) value ? "true" : "false"; + if (value instanceof Float || value instanceof Double) { + double number = ((Number) value).doubleValue(); + if (Double.isNaN(number)) return "NaN"; + if (number == Double.POSITIVE_INFINITY) return "Infinity"; + if (number == Double.NEGATIVE_INFINITY) return "-Infinity"; + if (number == 0) return "0"; + BigDecimal decimal = value instanceof Float + ? new BigDecimal(Float.toString((Float) value)) + : BigDecimal.valueOf(number); + decimal = decimal.stripTrailingZeros(); + double absolute = Math.abs(number); + return absolute >= 1e21 || absolute < 1e-6 + ? decimal.toString().replace("E", "e") + : decimal.toPlainString(); + } + if (value instanceof List) { + @SuppressWarnings("unchecked") + List values = (List) value; + return String.join(",", values.stream().map(Bucketer::javascriptString).toArray(String[]::new)); + } + if (value instanceof Map) return "[object Object]"; + return value.toString(); } } diff --git a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/ChildInstance.java b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/ChildInstance.java index a97cebe..cc51d05 100644 --- a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/ChildInstance.java +++ b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/ChildInstance.java @@ -5,6 +5,7 @@ import java.util.Map; import java.util.HashMap; import java.util.List; +import java.util.ArrayList; import com.featurevisor.sdk.EvaluatedFeatures; /** @@ -16,11 +17,12 @@ public class ChildInstance { private Map context; private Map sticky; private Emitter emitter; + private final List parentUnsubscribers = new ArrayList<>(); /** * Constructor */ - public ChildInstance(Featurevisor parent, Map context, Map sticky) { + ChildInstance(Featurevisor parent, Map context, Map sticky) { this.parent = parent; this.context = context != null ? new HashMap<>(context) : new HashMap<>(); this.sticky = sticky; @@ -30,18 +32,34 @@ public ChildInstance(Featurevisor parent, Map context, Map { + if (!active[0]) { + return; + } + active[0] = false; + parentUnsubscribe.unsubscribe(); + this.parentUnsubscribers.remove(holder[0]); + }; + this.parentUnsubscribers.add(holder[0]); + return holder[0]; } /** * Close instance */ public void close() { + for (FeaturevisorUnsubscribe unsubscribe : new ArrayList<>(this.parentUnsubscribers)) { + unsubscribe.unsubscribe(); + } + this.parentUnsubscribers.clear(); this.emitter.clearAll(); } @@ -56,11 +74,11 @@ public void setContext(Map context, boolean replace) { this.context.putAll(context); } - Emitter.EventDetails eventDetails = new Emitter.EventDetails(); + FeaturevisorEventDetails eventDetails = new FeaturevisorEventDetails(); eventDetails.put("context", this.context); eventDetails.put("replaced", replace); - this.emitter.trigger(Emitter.EventName.CONTEXT_SET, eventDetails); + this.emitter.trigger(FeaturevisorEventName.CONTEXT_SET, eventDetails); } public void setContext(Map context) { @@ -75,7 +93,7 @@ public Map getContext(Map context) { } public Map getContext() { - return new HashMap<>(this.context); + return this.parent.getContext(new HashMap<>(this.context)); } /** @@ -92,10 +110,10 @@ public void setSticky(Map sticky, boolean replace) { this.sticky.putAll(sticky); } - Emitter.EventDetails params = Events.getParamsForStickySetEvent( + FeaturevisorEventDetails params = Events.getParamsForStickySetEvent( previousStickyFeatures, this.sticky, replace); - this.emitter.trigger(Emitter.EventName.STICKY_SET, params); + this.emitter.trigger(FeaturevisorEventName.STICKY_SET, params); } public void setSticky(Map sticky) { @@ -105,6 +123,22 @@ public void setSticky(Map sticky) { /** * Flag */ + public Evaluation evaluateFlag(String featureKey, Map context, Featurevisor.OverrideOptions options) { + return this.parent.evaluateFlag( + featureKey, + mergeContexts(this.context, context), + mergeOverrideOptions(options) + ); + } + + public Evaluation evaluateFlag(String featureKey, Map context) { + return evaluateFlag(featureKey, context, null); + } + + public Evaluation evaluateFlag(String featureKey) { + return evaluateFlag(featureKey, null, null); + } + public boolean isEnabled(String featureKey, Map context, Featurevisor.OverrideOptions options) { return this.parent.isEnabled( featureKey, @@ -124,6 +158,22 @@ public boolean isEnabled(String featureKey) { /** * Variation */ + public Evaluation evaluateVariation(String featureKey, Map context, Featurevisor.OverrideOptions options) { + return this.parent.evaluateVariation( + featureKey, + mergeContexts(this.context, context), + mergeOverrideOptions(options) + ); + } + + public Evaluation evaluateVariation(String featureKey, Map context) { + return evaluateVariation(featureKey, context, null); + } + + public Evaluation evaluateVariation(String featureKey) { + return evaluateVariation(featureKey, null, null); + } + public String getVariation(String featureKey, Map context, Featurevisor.OverrideOptions options) { return this.parent.getVariation( featureKey, @@ -143,6 +193,23 @@ public String getVariation(String featureKey) { /** * Variable */ + public Evaluation evaluateVariable(String featureKey, String variableKey, Map context, Featurevisor.OverrideOptions options) { + return this.parent.evaluateVariable( + featureKey, + variableKey, + mergeContexts(this.context, context), + mergeOverrideOptions(options) + ); + } + + public Evaluation evaluateVariable(String featureKey, String variableKey, Map context) { + return evaluateVariable(featureKey, variableKey, context, null); + } + + public Evaluation evaluateVariable(String featureKey, String variableKey) { + return evaluateVariable(featureKey, variableKey, null, null); + } + public Object getVariable(String featureKey, String variableKey, Map context, Featurevisor.OverrideOptions options) { return this.parent.getVariable( featureKey, diff --git a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/CompareVersions.java b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/CompareVersions.java index 1987b78..7cf44c3 100644 --- a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/CompareVersions.java +++ b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/CompareVersions.java @@ -9,7 +9,7 @@ * Utility class for comparing semantic versions * Based on the TypeScript implementation from: https://github.com/omichelsen/compare-versions */ -public class CompareVersions { +final class CompareVersions { private static final Pattern SEMVER_PATTERN = Pattern.compile( "^[v^~<>=]*?(\\d+)(?:\\.([x*]|\\d+)(?:\\.([x*]|\\d+)(?:\\.([x*]|\\d+))?(?:-([\\da-z\\-]+(?:\\.[\\da-z\\-]+)*))?(?:\\+[\\da-z\\-]+(?:\\.[\\da-z\\-]+)*)?)?)?$", @@ -57,10 +57,7 @@ private static List validateAndParse(String version) { List result = new ArrayList<>(); for (int i = 1; i <= match.groupCount(); i++) { - String group = match.group(i); - if (group != null) { - result.add(group); - } + result.add(match.group(i)); } return result; } @@ -107,8 +104,8 @@ private static int compareSegments(List a, List b) { int maxLength = Math.max(a.size(), b.size()); for (int i = 0; i < maxLength; i++) { - String aVal = i < a.size() ? a.get(i) : "0"; - String bVal = i < b.size() ? b.get(i) : "0"; + String aVal = i < a.size() && a.get(i) != null ? a.get(i) : "0"; + String bVal = i < b.size() && b.get(i) != null ? b.get(i) : "0"; int r = compareStrings(aVal, bVal); if (r != 0) return r; diff --git a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/Conditions.java b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/Conditions.java index 6db1732..cee2eb6 100644 --- a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/Conditions.java +++ b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/Conditions.java @@ -4,10 +4,11 @@ import com.featurevisor.sdk.Operator; import java.util.Map; import java.util.List; -import java.util.regex.Pattern; import java.util.Date; -import java.text.SimpleDateFormat; -import java.text.ParseException; +import java.util.regex.Pattern; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.format.DateTimeParseException; /** * Conditions utility for Featurevisor SDK @@ -18,7 +19,7 @@ public class Conditions { * Functional interface for getting regex patterns. */ @FunctionalInterface - public interface GetRegex { + interface GetRegex { Pattern getRegex(String regexString, String regexFlags); } @@ -58,7 +59,7 @@ private static boolean pathExists(Map context, String path) { * @param getRegex Function to get regex patterns * @return True if the condition is matched */ - public static boolean conditionIsMatched( + static boolean conditionIsMatched( Condition condition, Map context, GetRegex getRegex) { @@ -134,11 +135,11 @@ public static boolean conditionIsMatched( switch (operator) { case EQUALS: - return equals(contextValue, value); + return pathExists(context, attribute) && equals(contextValue, value); case NOT_EQUALS: - return !equals(contextValue, value); + return !pathExists(context, attribute) || !equals(contextValue, value); case IN: - return in(contextValue, value); + return pathExists(context, attribute) && in(contextValue, value); case NOT_IN: // Match PHP implementation: check if value is array and context value is string/numeric/null if (value instanceof List && @@ -150,7 +151,7 @@ public static boolean conditionIsMatched( case CONTAINS: return contains(contextValue, value); case NOT_CONTAINS: - return !contains(contextValue, value); + return contextValue instanceof String && value instanceof String && !contains(contextValue, value); case STARTS_WITH: return startsWith(contextValue, value); case ENDS_WITH: @@ -170,15 +171,15 @@ public static boolean conditionIsMatched( case MATCHES: return matches(contextValue, value, regexFlags, getRegex); case NOT_MATCHES: - return !matches(contextValue, value, regexFlags, getRegex); + return contextValue instanceof String && value instanceof String && !matches(contextValue, value, regexFlags, getRegex); case INCLUDES: return includes(contextValue, value); case NOT_INCLUDES: - return !includes(contextValue, value); + return contextValue instanceof List && isPrimitive(value) && !includes(contextValue, value); case SEMVER_EQUALS: return semverEquals(contextValue, value); case SEMVER_NOT_EQUALS: - return !semverEquals(contextValue, value); + return contextValue instanceof String && value instanceof String && !semverEquals(contextValue, value); case SEMVER_GREATER_THAN: return semverGreaterThan(contextValue, value); case SEMVER_GREATER_THAN_OR_EQUALS: @@ -208,9 +209,9 @@ public static boolean allConditionsAreMatched(Object conditions, Map()); datafile.setSegments(new java.util.HashMap<>()); - return new DatafileReader(new DatafileReader.DatafileReaderOptions() + return new InstanceEvaluationDataProvider(new InstanceEvaluationDataProvider.InstanceEvaluationDataProviderOptions() .datafile(datafile) - .logger(Logger.createLogger(new Logger.CreateLoggerOptions()))) + .diagnostics(DiagnosticReporter.createDiagnosticReporter(new DiagnosticReporter.DiagnosticReporterOptions()))) .allConditionsAreMatched(conditions, context); } @@ -221,7 +222,18 @@ private static boolean equals(Object contextValue, Object conditionValue) { if (contextValue == null || conditionValue == null) { return false; } - return contextValue.equals(conditionValue); + if (contextValue instanceof Number && conditionValue instanceof Number) { + double left = ((Number) contextValue).doubleValue(); + double right = ((Number) conditionValue).doubleValue(); + return !Double.isNaN(left) && !Double.isNaN(right) && left == right; + } + if (contextValue instanceof String && conditionValue instanceof String) { + return contextValue.equals(conditionValue); + } + if (contextValue instanceof Boolean && conditionValue instanceof Boolean) { + return contextValue.equals(conditionValue); + } + return false; } private static boolean in(Object contextValue, Object conditionValue) { @@ -233,14 +245,14 @@ private static boolean in(Object contextValue, Object conditionValue) { if (contextValue == null) { @SuppressWarnings("unchecked") List values = (List) conditionValue; - return values.contains(null); + return values.stream().anyMatch(item -> equals(item, null)); } // Handle string, numeric, or null context values if (contextValue instanceof String || contextValue instanceof Number || contextValue == null) { @SuppressWarnings("unchecked") List values = (List) conditionValue; - return values.contains(contextValue); + return values.stream().anyMatch(item -> equals(item, contextValue)); } return false; @@ -305,16 +317,20 @@ private static boolean matches(Object contextValue, Object conditionValue, Strin return false; } - return regex.matcher((String) contextValue).matches(); + return regex.matcher((String) contextValue).find(); } private static boolean includes(Object contextValue, Object conditionValue) { - if (!(contextValue instanceof List) || !(conditionValue instanceof String)) { + if (!(contextValue instanceof List) || !isPrimitive(conditionValue)) { return false; } @SuppressWarnings("unchecked") List list = (List) contextValue; - return list.contains(conditionValue); + return list.stream().anyMatch(item -> equals(item, conditionValue)); + } + + private static boolean isPrimitive(Object value) { + return value == null || value instanceof String || value instanceof Number || value instanceof Boolean; } private static boolean semverEquals(Object contextValue, Object conditionValue) { @@ -353,51 +369,39 @@ private static boolean semverLessThanOrEquals(Object contextValue, Object condit } private static boolean before(Object contextValue, Object conditionValue) { - Date contextDate = parseDate(contextValue); - Date conditionDate = parseDate(conditionValue); + Instant contextDate = parseDate(contextValue); + Instant conditionDate = parseDate(conditionValue); if (contextDate == null || conditionDate == null) { return false; } - return contextDate.before(conditionDate); + return contextDate.isBefore(conditionDate); } private static boolean after(Object contextValue, Object conditionValue) { - Date contextDate = parseDate(contextValue); - Date conditionDate = parseDate(conditionValue); + Instant contextDate = parseDate(contextValue); + Instant conditionDate = parseDate(conditionValue); if (contextDate == null || conditionDate == null) { return false; } - return contextDate.after(conditionDate); + return contextDate.isAfter(conditionDate); } - private static Date parseDate(Object value) { + private static Instant parseDate(Object value) { + if (value instanceof Instant) { + return (Instant) value; + } if (value instanceof Date) { - return (Date) value; + return ((Date) value).toInstant(); } if (value instanceof String) { try { - // Try ISO 8601 format first - SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); - isoFormat.setLenient(false); - return isoFormat.parse((String) value); - } catch (ParseException e1) { - try { - // Try alternative ISO format without Z - SimpleDateFormat altFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); - altFormat.setLenient(false); - return altFormat.parse((String) value); - } catch (ParseException e2) { - try { - // Try legacy Date constructor as fallback - return new Date((String) value); - } catch (Exception e3) { - return null; - } - } + return OffsetDateTime.parse((String) value).toInstant(); + } catch (DateTimeParseException ignored) { + return null; } } return null; diff --git a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/ConfigureBucketKey.java b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/ConfigureBucketKey.java new file mode 100644 index 0000000..9dc32e9 --- /dev/null +++ b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/ConfigureBucketKey.java @@ -0,0 +1,6 @@ +package com.featurevisor.sdk; + +@FunctionalInterface +public interface ConfigureBucketKey { + String configure(ConfigureBucketKeyOptions options); +} diff --git a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/ConfigureBucketKeyOptions.java b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/ConfigureBucketKeyOptions.java new file mode 100644 index 0000000..1435e83 --- /dev/null +++ b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/ConfigureBucketKeyOptions.java @@ -0,0 +1,31 @@ +package com.featurevisor.sdk; + +import java.util.Map; + +public final class ConfigureBucketKeyOptions { + private String featureKey; + private Map context; + private Bucket bucketBy; + private String bucketKey; + + public ConfigureBucketKeyOptions( + String featureKey, + Map context, + Bucket bucketBy, + String bucketKey) { + this.featureKey = featureKey; + this.context = context; + this.bucketBy = bucketBy; + this.bucketKey = bucketKey; + } + + public String getFeatureKey() { return featureKey; } + public Map getContext() { return context; } + public Bucket getBucketBy() { return bucketBy; } + public String getBucketKey() { return bucketKey; } + + public void setFeatureKey(String featureKey) { this.featureKey = featureKey; } + public void setContext(Map context) { this.context = context; } + public void setBucketBy(Bucket bucketBy) { this.bucketBy = bucketBy; } + public void setBucketKey(String bucketKey) { this.bucketKey = bucketKey; } +} diff --git a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/ConfigureBucketValue.java b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/ConfigureBucketValue.java new file mode 100644 index 0000000..88ec484 --- /dev/null +++ b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/ConfigureBucketValue.java @@ -0,0 +1,6 @@ +package com.featurevisor.sdk; + +@FunctionalInterface +public interface ConfigureBucketValue { + int configure(ConfigureBucketValueOptions options); +} diff --git a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/ConfigureBucketValueOptions.java b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/ConfigureBucketValueOptions.java new file mode 100644 index 0000000..e0d1b06 --- /dev/null +++ b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/ConfigureBucketValueOptions.java @@ -0,0 +1,31 @@ +package com.featurevisor.sdk; + +import java.util.Map; + +public final class ConfigureBucketValueOptions { + private String featureKey; + private String bucketKey; + private Map context; + private int bucketValue; + + public ConfigureBucketValueOptions( + String featureKey, + String bucketKey, + Map context, + int bucketValue) { + this.featureKey = featureKey; + this.bucketKey = bucketKey; + this.context = context; + this.bucketValue = bucketValue; + } + + public String getFeatureKey() { return featureKey; } + public String getBucketKey() { return bucketKey; } + public Map getContext() { return context; } + public int getBucketValue() { return bucketValue; } + + public void setFeatureKey(String featureKey) { this.featureKey = featureKey; } + public void setBucketKey(String bucketKey) { this.bucketKey = bucketKey; } + public void setContext(Map context) { this.context = context; } + public void setBucketValue(int bucketValue) { this.bucketValue = bucketValue; } +} diff --git a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/ContextUtils.java b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/ContextUtils.java index 7340847..ef7f766 100644 --- a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/ContextUtils.java +++ b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/ContextUtils.java @@ -5,7 +5,7 @@ /** * Utility class for working with context objects */ -public class ContextUtils { +final class ContextUtils { /** * Get a value from a context object using a dot-separated path diff --git a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/Logger.java b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/DiagnosticReporter.java similarity index 62% rename from featurevisor-sdk/src/main/java/com/featurevisor/sdk/Logger.java rename to featurevisor-sdk/src/main/java/com/featurevisor/sdk/DiagnosticReporter.java index 5bcd098..4f3233f 100644 --- a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/Logger.java +++ b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/DiagnosticReporter.java @@ -4,33 +4,34 @@ import java.util.HashMap; /** - * Logger for Featurevisor SDK + * DiagnosticReporter for Featurevisor SDK */ -final class Logger { +final class DiagnosticReporter { private static final FeaturevisorLogLevel[] ALL_LEVELS = { FeaturevisorLogLevel.DEBUG, FeaturevisorLogLevel.INFO, FeaturevisorLogLevel.WARN, FeaturevisorLogLevel.ERROR, FeaturevisorLogLevel.FATAL }; private static final FeaturevisorLogLevel DEFAULT_LEVEL = FeaturevisorLogLevel.INFO; - private static final String LOGGER_PREFIX = "[Featurevisor]"; + private static final String DIAGNOSTIC_PREFIX = "[Featurevisor]"; private FeaturevisorLogLevel level; - private LogHandler handler; + private DiagnosticOutputHandler handler; + private boolean filter; - interface LogHandler { + interface DiagnosticOutputHandler { void handle(FeaturevisorLogLevel level, String message, Map details); } - static class CreateLoggerOptions { + static class DiagnosticReporterOptions { private FeaturevisorLogLevel level; - private LogHandler handler; + private DiagnosticOutputHandler handler; - public CreateLoggerOptions level(FeaturevisorLogLevel level) { + public DiagnosticReporterOptions level(FeaturevisorLogLevel level) { this.level = level; return this; } - public CreateLoggerOptions handler(LogHandler handler) { + public DiagnosticReporterOptions handler(DiagnosticOutputHandler handler) { this.handler = handler; return this; } @@ -39,34 +40,39 @@ public FeaturevisorLogLevel getLevel() { return level; } - public LogHandler getHandler() { + public DiagnosticOutputHandler getHandler() { return handler; } } - Logger() { + DiagnosticReporter() { this.level = DEFAULT_LEVEL; - this.handler = this::defaultLogHandler; + this.handler = this::defaultDiagnosticOutputHandler; + this.filter = true; } - Logger(FeaturevisorLogLevel level) { + DiagnosticReporter(FeaturevisorLogLevel level) { this.level = level != null ? level : DEFAULT_LEVEL; - this.handler = this::defaultLogHandler; + this.handler = this::defaultDiagnosticOutputHandler; + this.filter = true; } - Logger(LogHandler handler) { + DiagnosticReporter(DiagnosticOutputHandler handler) { this.level = DEFAULT_LEVEL; - this.handler = handler != null ? handler : this::defaultLogHandler; + this.handler = handler != null ? handler : this::defaultDiagnosticOutputHandler; + this.filter = handler == null; } - Logger(FeaturevisorLogLevel level, LogHandler handler) { + DiagnosticReporter(FeaturevisorLogLevel level, DiagnosticOutputHandler handler) { this.level = level != null ? level : DEFAULT_LEVEL; - this.handler = handler != null ? handler : this::defaultLogHandler; + this.handler = handler != null ? handler : this::defaultDiagnosticOutputHandler; + this.filter = handler == null; } - Logger(CreateLoggerOptions options) { + DiagnosticReporter(DiagnosticReporterOptions options) { this.level = options.getLevel() != null ? options.getLevel() : DEFAULT_LEVEL; - this.handler = options.getHandler() != null ? options.getHandler() : this::defaultLogHandler; + this.handler = options.getHandler() != null ? options.getHandler() : this::defaultDiagnosticOutputHandler; + this.filter = options.getHandler() == null; } public void debug(String message) { @@ -110,9 +116,14 @@ public void fatal(String message, Map details) { } public void log(FeaturevisorLogLevel logLevel, String message, Map details) { - if (shouldLog(logLevel)) { - handler.handle(logLevel, message, details); + if (filter && !shouldLog(logLevel)) { + return; } + + // Custom handlers are evaluator sinks and receive every diagnostic. + // Featurevisor applies filters independently for module subscribers, + // the main diagnostic handler, console output, and error events. + handler.handle(logLevel, message, details); } private boolean shouldLog(FeaturevisorLogLevel logLevel) { @@ -132,13 +143,13 @@ private int getLevelIndex(FeaturevisorLogLevel level) { return 0; } - private void defaultLogHandler(FeaturevisorLogLevel level, String message, Map details) { + private void defaultDiagnosticOutputHandler(FeaturevisorLogLevel level, String message, Map details) { writeToConsole(level, message, details); } static void writeToConsole(FeaturevisorLogLevel level, String message, Map details) { String levelStr = level.name().toLowerCase(); - String logMessage = String.format("%s %s: %s", LOGGER_PREFIX, levelStr, message); + String logMessage = String.format("%s %s: %s", DIAGNOSTIC_PREFIX, levelStr, message); if (details != null && !details.isEmpty()) { logMessage += " " + details.toString(); @@ -155,19 +166,20 @@ public void setLevel(FeaturevisorLogLevel level) { this.level = level != null ? level : DEFAULT_LEVEL; } - LogHandler getHandler() { + DiagnosticOutputHandler getHandler() { return handler; } - void setHandler(LogHandler handler) { - this.handler = handler != null ? handler : this::defaultLogHandler; + void setHandler(DiagnosticOutputHandler handler) { + this.handler = handler != null ? handler : this::defaultDiagnosticOutputHandler; + this.filter = handler == null; } - static Logger createLogger() { - return createLogger(new CreateLoggerOptions()); + static DiagnosticReporter createDiagnosticReporter() { + return createDiagnosticReporter(new DiagnosticReporterOptions()); } - static Logger createLogger(CreateLoggerOptions options) { - return new Logger(options); + static DiagnosticReporter createDiagnosticReporter(DiagnosticReporterOptions options) { + return new DiagnosticReporter(options); } } diff --git a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/Emitter.java b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/Emitter.java index f89a0b9..8c1aa43 100644 --- a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/Emitter.java +++ b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/Emitter.java @@ -1,155 +1,57 @@ package com.featurevisor.sdk; -import java.util.Map; import java.util.HashMap; import java.util.List; -import java.util.ArrayList; +import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.function.Consumer; - -/** - * Event emitter for Featurevisor SDK - * Handles event subscription and triggering - */ -public class Emitter { - - /** - * Event names that can be emitted - */ - public enum EventName { - DATAFILE_SET("datafile_set"), - CONTEXT_SET("context_set"), - STICKY_SET("sticky_set"), - ERROR("error"); - - private final String value; - - EventName(String value) { - this.value = value; - } - - public String getValue() { - return value; - } - - public static EventName fromString(String value) { - for (EventName eventName : EventName.values()) { - if (eventName.value.equals(value)) { - return eventName; - } - } - throw new IllegalArgumentException("Unknown event name: " + value); - } - } - - /** - * Event details type - */ - public static class EventDetails extends HashMap { - public EventDetails() { - super(); - } - public EventDetails(Map map) { - super(map); - } - } - - /** - * Event callback interface - */ - @FunctionalInterface - public interface EventCallback { - void call(EventDetails details); - } +final class Emitter { + private final Map> listeners = + new HashMap<>(); - /** - * Unsubscribe function interface - */ - @FunctionalInterface - public interface UnsubscribeFunction { - void unsubscribe(); - } - - private final Map> listeners; - - public Emitter() { - this.listeners = new HashMap<>(); - } - - /** - * Subscribe to an event - * @param eventName The event name to subscribe to - * @param callback The callback function to execute when the event is triggered - * @return An unsubscribe function to remove the listener - */ - public UnsubscribeFunction on(EventName eventName, EventCallback callback) { - if (!listeners.containsKey(eventName)) { - listeners.put(eventName, new CopyOnWriteArrayList<>()); - } - - List eventListeners = listeners.get(eventName); - eventListeners.add(callback); - - // Track if the subscription is still active - final boolean[] isActive = {true}; + FeaturevisorUnsubscribe on( + FeaturevisorEventName eventName, + FeaturevisorEventHandler callback) { + listeners.computeIfAbsent(eventName, ignored -> new CopyOnWriteArrayList<>()).add(callback); + final boolean[] active = {true}; return () -> { - if (!isActive[0]) { + if (!active[0]) { return; } - - isActive[0] = false; - - List currentListeners = listeners.get(eventName); + active[0] = false; + List currentListeners = listeners.get(eventName); if (currentListeners != null) { currentListeners.remove(callback); } }; } - /** - * Trigger an event with optional details - * @param eventName The event name to trigger - * @param details Optional event details - */ - public void trigger(EventName eventName, EventDetails details) { - List eventListeners = listeners.get(eventName); - + void trigger(FeaturevisorEventName eventName, FeaturevisorEventDetails details) { + List eventListeners = listeners.get(eventName); if (eventListeners == null) { return; } - for (EventCallback listener : eventListeners) { + for (FeaturevisorEventHandler listener : eventListeners) { try { - listener.call(details); - } catch (Exception err) { - System.err.println("Error in event listener: " + err.getMessage()); - err.printStackTrace(); + listener.handle(details); + } catch (Exception error) { + System.err.println("Error in event listener: " + error.getMessage()); + error.printStackTrace(); } } } - /** - * Trigger an event with empty details - * @param eventName The event name to trigger - */ - public void trigger(EventName eventName) { - trigger(eventName, new EventDetails()); + void trigger(FeaturevisorEventName eventName) { + trigger(eventName, new FeaturevisorEventDetails()); } - /** - * Clear all event listeners - */ - public void clearAll() { + void clearAll() { listeners.clear(); } - /** - * Get the current listeners (for testing purposes) - * @return A copy of the listeners map - */ - Map> getListeners() { + Map> getListeners() { return new HashMap<>(listeners); } } diff --git a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/Evaluate.java b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/Evaluate.java index f01afb4..138d582 100644 --- a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/Evaluate.java +++ b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/Evaluate.java @@ -34,12 +34,12 @@ public static Evaluation evaluateWithModules(EvaluateOptions opts) { // default: variation if (options.getDefaultVariationValue() != null && Evaluation.TYPE_VARIATION.equals(evaluation.getType()) && - evaluation.getVariationValue() == null) { + evaluation.getVariationValue() == null && evaluation.getVariation() == null) { evaluation.variationValue(options.getDefaultVariationValue()); } // default: variable - if (options.getDefaultVariableValue() != null && + if (options.hasDefaultVariableValue() && Evaluation.TYPE_VARIABLE.equals(evaluation.getType()) && evaluation.getVariableValue() == null) { evaluation.variableValue(options.getDefaultVariableValue()); @@ -55,7 +55,7 @@ public static Evaluation evaluateWithModules(EvaluateOptions opts) { String type = opts.getType(); String featureKey = opts.getFeatureKey(); String variableKey = opts.getVariableKey(); - Logger logger = opts.getLogger(); + DiagnosticReporter diagnostics = opts.getDiagnostics(); Evaluation evaluation = new Evaluation(type, featureKey, variableKey) .reason(Evaluation.REASON_ERROR) @@ -65,7 +65,7 @@ public static Evaluation evaluateWithModules(EvaluateOptions opts) { details.put("featureKey", featureKey); details.put("variableKey", variableKey); details.put("error", e.getMessage()); - logger.error("error during evaluation", details); + diagnostics.error("error during evaluation", details); return evaluation; } @@ -80,7 +80,7 @@ public static Evaluation evaluate(EvaluateOptions options) { String type = options.getType(); String featureKey = options.getFeatureKey(); String variableKey = options.getVariableKey(); - Logger logger = options.getLogger(); + DiagnosticReporter diagnostics = options.getDiagnostics(); Evaluation evaluation; @@ -90,9 +90,7 @@ public static Evaluation evaluate(EvaluateOptions options) { if (!Evaluation.TYPE_FLAG.equals(type)) { // needed by variation and variable evaluations - flag = options.getFlagEvaluation() != null ? - options.getFlagEvaluation() : - evaluate(options.copy().type(Evaluation.TYPE_FLAG)); + flag = evaluate(options.copy().type(Evaluation.TYPE_FLAG)); Evaluation disabledEvaluation = EvaluateDisabled.evaluateDisabled(options, flag); if (disabledEvaluation != null) { @@ -152,7 +150,7 @@ public static Evaluation evaluate(EvaluateOptions options) { details.put("featureKey", featureKey); details.put("bucketKey", bucketKey); details.put("bucketValue", bucketValue); - logger.debug("nothing matched", details); + diagnostics.debug("nothing matched", details); return evaluation; } catch (Exception e) { @@ -164,7 +162,7 @@ public static Evaluation evaluate(EvaluateOptions options) { details.put("featureKey", featureKey); details.put("variableKey", variableKey); details.put("error", e.getMessage()); - logger.error("error during evaluation", details); + diagnostics.error("error during evaluation", details); return evaluation; } @@ -180,8 +178,8 @@ private static Evaluation evaluateRequired(EvaluateOptions options, Feature feat String type = options.getType(); String featureKey = options.getFeatureKey(); String variableKey = options.getVariableKey(); - Logger logger = options.getLogger(); - DatafileReader datafileReader = options.getDatafileReader(); + DiagnosticReporter diagnostics = options.getDiagnostics(); + InstanceEvaluationDataProvider evaluationData = options.getInstanceEvaluationDataProvider(); // Check if required features are enabled List requiredList = feature.getRequired(); @@ -195,7 +193,7 @@ private static Evaluation evaluateRequired(EvaluateOptions options, Feature feat for (Object required : requiredList) { if (required instanceof String) { String requiredFeatureKey = (String) required; - Feature requiredFeature = datafileReader.getFeature(requiredFeatureKey); + Feature requiredFeature = evaluationData.getFeature(requiredFeatureKey); if (requiredFeature == null) { Evaluation evaluation = new Evaluation(type, featureKey, variableKey) @@ -205,7 +203,7 @@ private static Evaluation evaluateRequired(EvaluateOptions options, Feature feat Map details = new HashMap<>(); details.put("featureKey", featureKey); details.put("requiredFeatureKey", requiredFeatureKey); - logger.debug("required feature not found", details); + diagnostics.debug("required feature not found", details); return evaluation; } @@ -223,7 +221,7 @@ private static Evaluation evaluateRequired(EvaluateOptions options, Feature feat Map details = new HashMap<>(); details.put("featureKey", featureKey); details.put("requiredFeatureKey", requiredFeatureKey); - logger.debug("required feature disabled", details); + diagnostics.debug("required feature disabled", details); return evaluation; } @@ -237,7 +235,7 @@ private static Evaluation evaluateRequired(EvaluateOptions options, Feature feat continue; } - Feature requiredFeature = datafileReader.getFeature(requiredFeatureKey); + Feature requiredFeature = evaluationData.getFeature(requiredFeatureKey); if (requiredFeature == null) { Evaluation evaluation = new Evaluation(type, featureKey, variableKey) @@ -247,7 +245,7 @@ private static Evaluation evaluateRequired(EvaluateOptions options, Feature feat Map details = new HashMap<>(); details.put("featureKey", featureKey); details.put("requiredFeatureKey", requiredFeatureKey); - logger.debug("required feature not found", details); + diagnostics.debug("required feature not found", details); return evaluation; } @@ -272,7 +270,7 @@ private static Evaluation evaluateRequired(EvaluateOptions options, Feature feat details.put("requiredFeatureKey", requiredFeatureKey); details.put("requiredVariation", requiredVariation); details.put("actualVariation", variationValue); - logger.debug("required feature variation mismatch", details); + diagnostics.debug("required feature variation mismatch", details); return evaluation; } diff --git a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/EvaluateByBucketing.java b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/EvaluateByBucketing.java index 2eb532a..a592936 100644 --- a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/EvaluateByBucketing.java +++ b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/EvaluateByBucketing.java @@ -68,20 +68,20 @@ public static EvaluateByBucketingResult evaluateByBucketing( String featureKey = options.getFeatureKey(); String variableKey = options.getVariableKey(); Map context = options.getContext(); - Logger logger = options.getLogger(); + DiagnosticReporter diagnostics = options.getDiagnostics(); ModulesManager modulesManager = options.getModulesManager(); - DatafileReader datafileReader = options.getDatafileReader(); + InstanceEvaluationDataProvider evaluationData = options.getInstanceEvaluationDataProvider(); // Get bucket key String bucketKey = Bucketer.getBucketKey(new Bucketer.GetBucketKeyOptions() .featureKey(featureKey) .bucketBy(feature.getBucketBy()) .context(context) - .logger(logger)); + .diagnostics(diagnostics)); // Apply bucket key modules if (modulesManager != null) { - bucketKey = modulesManager.executeBucketKeyModules(new ModulesManager.ConfigureBucketKeyOptions( + bucketKey = modulesManager.executeBucketKeyModules(new ConfigureBucketKeyOptions( featureKey, context, feature.getBucketBy(), bucketKey)); } @@ -90,7 +90,7 @@ public static EvaluateByBucketingResult evaluateByBucketing( // Apply bucket value modules if (modulesManager != null) { - bucketValue = modulesManager.executeBucketValueModules(new ModulesManager.ConfigureBucketValueOptions( + bucketValue = modulesManager.executeBucketValueModules(new ConfigureBucketValueOptions( featureKey, bucketKey, context, bucketValue)); } @@ -99,12 +99,12 @@ public static EvaluateByBucketingResult evaluateByBucketing( Allocation matchedAllocation = null; if (!Evaluation.TYPE_FLAG.equals(type)) { - matchedTraffic = datafileReader.getMatchedTraffic(feature.getTraffic(), context); + matchedTraffic = evaluationData.getMatchedTraffic(feature.getTraffic(), context); if (matchedTraffic != null) { - matchedAllocation = datafileReader.getMatchedAllocation(matchedTraffic, bucketValue); + matchedAllocation = evaluationData.getMatchedAllocation(matchedTraffic, bucketValue); } } else { - matchedTraffic = datafileReader.getMatchedTraffic(feature.getTraffic(), context); + matchedTraffic = evaluationData.getMatchedTraffic(feature.getTraffic(), context); } @@ -130,7 +130,7 @@ public static EvaluateByBucketingResult evaluateByBucketing( details.put("featureKey", featureKey); details.put("bucketKey", bucketKey); details.put("bucketValue", bucketValue); - logger.debug("matched rule with 0 percentage", details); + diagnostics.debug("matched rule with 0 percentage", details); result.setEvaluation(evaluation); return result; @@ -163,7 +163,7 @@ public static EvaluateByBucketingResult evaluateByBucketing( details.put("featureKey", featureKey); details.put("bucketKey", bucketKey); details.put("bucketValue", bucketValue); - logger.debug("matched", details); + diagnostics.debug("matched", details); result.setEvaluation(evaluation); return result; @@ -180,7 +180,7 @@ public static EvaluateByBucketingResult evaluateByBucketing( details.put("featureKey", featureKey); details.put("bucketKey", bucketKey); details.put("bucketValue", bucketValue); - logger.debug("not matched", details); + diagnostics.debug("not matched", details); result.setEvaluation(evaluation); return result; @@ -200,7 +200,7 @@ public static EvaluateByBucketingResult evaluateByBucketing( details.put("featureKey", featureKey); details.put("bucketKey", bucketKey); details.put("bucketValue", bucketValue); - logger.debug("override from rule", details); + diagnostics.debug("override from rule", details); result.setEvaluation(evaluation); return result; @@ -220,7 +220,7 @@ public static EvaluateByBucketingResult evaluateByBucketing( details.put("featureKey", featureKey); details.put("bucketKey", bucketKey); details.put("bucketValue", bucketValue); - logger.debug("matched traffic", details); + diagnostics.debug("matched traffic", details); result.setEvaluation(evaluation); return result; @@ -252,7 +252,7 @@ public static EvaluateByBucketingResult evaluateByBucketing( details.put("featureKey", featureKey); details.put("bucketKey", bucketKey); details.put("bucketValue", bucketValue); - logger.debug("override from rule", details); + diagnostics.debug("override from rule", details); result.setEvaluation(evaluation); return result; @@ -282,7 +282,7 @@ public static EvaluateByBucketingResult evaluateByBucketing( details.put("featureKey", featureKey); details.put("bucketKey", bucketKey); details.put("bucketValue", bucketValue); - logger.debug("regular allocation", details); + diagnostics.debug("regular allocation", details); result.setEvaluation(evaluation); return result; @@ -312,10 +312,10 @@ public static EvaluateByBucketingResult evaluateByBucketing( } } - matches = datafileReader.allConditionsAreMatched(conditions, context); + matches = evaluationData.allConditionsAreMatched(conditions, context); } else if (override.getSegments() != null) { - Object parsedSegments = datafileReader.parseSegmentsIfStringified(override.getSegments()); - matches = datafileReader.allSegmentsAreMatched(parsedSegments, context); + Object parsedSegments = evaluationData.parseSegmentsIfStringified(override.getSegments()); + matches = evaluationData.allSegmentsAreMatched(parsedSegments, context); } if (matches) { @@ -334,7 +334,7 @@ public static EvaluateByBucketingResult evaluateByBucketing( details.put("variableKey", variableKey); details.put("bucketKey", bucketKey); details.put("bucketValue", bucketValue); - logger.debug("variable override from rule", details); + diagnostics.debug("variable override from rule", details); result.setEvaluation(evaluation); return result; @@ -360,7 +360,7 @@ public static EvaluateByBucketingResult evaluateByBucketing( details.put("variableKey", variableKey); details.put("bucketKey", bucketKey); details.put("bucketValue", bucketValue); - logger.debug("variable override from rule", details); + diagnostics.debug("variable override from rule", details); result.setEvaluation(evaluation); return result; @@ -407,12 +407,12 @@ public static EvaluateByBucketingResult evaluateByBucketing( conditions = override.getConditions(); } } - matches = datafileReader.allConditionsAreMatched(conditions, context); + matches = evaluationData.allConditionsAreMatched(conditions, context); } // Check segments else if (override.getSegments() != null) { - Object parsedSegments = datafileReader.parseSegmentsIfStringified(override.getSegments()); - matches = datafileReader.allSegmentsAreMatched(parsedSegments, context); + Object parsedSegments = evaluationData.parseSegmentsIfStringified(override.getSegments()); + matches = evaluationData.allSegmentsAreMatched(parsedSegments, context); } if (matches) { @@ -431,7 +431,7 @@ else if (override.getSegments() != null) { details.put("variableKey", variableKey); details.put("bucketKey", bucketKey); details.put("bucketValue", bucketValue); - logger.debug("variable override", details); + diagnostics.debug("variable override", details); result.setEvaluation(evaluation); return result; @@ -457,7 +457,7 @@ else if (override.getSegments() != null) { details.put("variableKey", variableKey); details.put("bucketKey", bucketKey); details.put("bucketValue", bucketValue); - logger.debug("variable from variation", details); + diagnostics.debug("variable from variation", details); result.setEvaluation(evaluation); return result; @@ -482,7 +482,7 @@ else if (override.getSegments() != null) { details.put("variableKey", variableKey); details.put("bucketKey", bucketKey); details.put("bucketValue", bucketValue); - logger.debug("variable default value", details); + diagnostics.debug("variable default value", details); result.setEvaluation(evaluation); return result; @@ -502,7 +502,7 @@ else if (override.getSegments() != null) { details.put("variableKey", variableKey); details.put("bucketKey", bucketKey); details.put("bucketValue", bucketValue); - logger.debug("variable not found", details); + diagnostics.debug("variable not found", details); result.setEvaluation(evaluation); return result; @@ -520,7 +520,7 @@ else if (override.getSegments() != null) { details.put("featureKey", featureKey); details.put("bucketKey", bucketKey); details.put("bucketValue", bucketValue); - logger.debug("no matched variation", details); + diagnostics.debug("no matched variation", details); result.setEvaluation(evaluation); return result; diff --git a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/EvaluateDisabled.java b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/EvaluateDisabled.java index 060874f..b6f5ace 100644 --- a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/EvaluateDisabled.java +++ b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/EvaluateDisabled.java @@ -12,16 +12,16 @@ final class EvaluateDisabled { /** * Evaluates a disabled feature and returns the appropriate evaluation result * - * @param options The evaluation options containing type, featureKey, datafileReader, variableKey, and logger + * @param options The evaluation options containing type, featureKey, evaluationData, variableKey, and diagnostics * @param flag The flag evaluation result * @return Evaluation result for disabled feature, or null if not disabled */ public static Evaluation evaluateDisabled(EvaluateOptions options, Evaluation flag) { String type = options.getType(); String featureKey = options.getFeatureKey(); - DatafileReader datafileReader = options.getDatafileReader(); + InstanceEvaluationDataProvider evaluationData = options.getInstanceEvaluationDataProvider(); String variableKey = options.getVariableKey(); - Logger logger = options.getLogger(); + DiagnosticReporter diagnostics = options.getDiagnostics(); if (!Evaluation.TYPE_FLAG.equals(type)) { if (flag != null && Boolean.FALSE.equals(flag.getEnabled())) { @@ -30,7 +30,7 @@ public static Evaluation evaluateDisabled(EvaluateOptions options, Evaluation fl .featureKey(featureKey) .reason(Evaluation.REASON_DISABLED); - Feature feature = datafileReader.getFeature(featureKey); + Feature feature = evaluationData.getFeature(featureKey); // serve variable default value if feature is disabled (if explicitly specified) if (Evaluation.TYPE_VARIABLE.equals(type)) { @@ -75,7 +75,7 @@ public static Evaluation evaluateDisabled(EvaluateOptions options, Evaluation fl .enabled(false); } - logger.debug("feature is disabled", null); + diagnostics.debug("feature is disabled", null); return evaluation; } diff --git a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/EvaluateForced.java b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/EvaluateForced.java index ea2b856..f92fc0b 100644 --- a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/EvaluateForced.java +++ b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/EvaluateForced.java @@ -45,7 +45,7 @@ public EvaluateForcedResult(Evaluation evaluation, Force force, Integer forceInd * Evaluates a forced feature and returns the appropriate evaluation result * This method matches the PHP implementation's evaluate() method * - * @param options The evaluation options containing type, featureKey, variableKey, context, logger, and datafileReader + * @param options The evaluation options containing type, featureKey, variableKey, context, diagnostics, and evaluationData * @param feature The feature to evaluate * @param variableSchema The variable schema (can be null) * @return EvaluateForcedResult containing evaluation, force, and forceIndex @@ -55,10 +55,10 @@ public static EvaluateForcedResult evaluate(EvaluateOptions options, Feature fea String featureKey = options.getFeatureKey(); String variableKey = options.getVariableKey(); Map context = options.getContext(); - Logger logger = options.getLogger(); - DatafileReader datafileReader = options.getDatafileReader(); + DiagnosticReporter diagnostics = options.getDiagnostics(); + InstanceEvaluationDataProvider evaluationData = options.getInstanceEvaluationDataProvider(); - DatafileReader.ForceResult forceResult = datafileReader.getMatchedForce(feature, context); + InstanceEvaluationDataProvider.ForceResult forceResult = evaluationData.getMatchedForce(feature, context); Force force = forceResult.getForce(); Integer forceIndex = forceResult.getForceIndex(); @@ -79,7 +79,7 @@ public static EvaluateForcedResult evaluate(EvaluateOptions options, Feature fea result.setEvaluation(evaluation); - logger.debug("forced enabled found", evaluationToMap(evaluation)); + diagnostics.debug("forced enabled found", evaluationToMap(evaluation)); return result; } @@ -105,7 +105,7 @@ public static EvaluateForcedResult evaluate(EvaluateOptions options, Feature fea result.setEvaluation(evaluation); - logger.debug("forced variation found", evaluationToMap(evaluation)); + diagnostics.debug("forced variation found", evaluationToMap(evaluation)); return result; } @@ -150,12 +150,12 @@ else if (force.getVariation() != null && feature.getVariations() != null) { // Check conditions if (override.getConditions() != null) { - matches = datafileReader.allConditionsAreMatched(override.getConditions(), context); + matches = evaluationData.allConditionsAreMatched(override.getConditions(), context); } // Check segments else if (override.getSegments() != null) { - Object parsedSegments = datafileReader.parseSegmentsIfStringified(override.getSegments()); - matches = datafileReader.allSegmentsAreMatched(parsedSegments, context); + Object parsedSegments = evaluationData.parseSegmentsIfStringified(override.getSegments()); + matches = evaluationData.allSegmentsAreMatched(parsedSegments, context); } if (matches) { @@ -180,7 +180,7 @@ else if (override.getSegments() != null) { result.setEvaluation(evaluation); - logger.debug("forced variable", evaluationToMap(evaluation)); + diagnostics.debug("forced variable", evaluationToMap(evaluation)); return result; } diff --git a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/EvaluateNotFound.java b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/EvaluateNotFound.java index a7ee38e..2a98937 100644 --- a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/EvaluateNotFound.java +++ b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/EvaluateNotFound.java @@ -2,6 +2,7 @@ import com.featurevisor.sdk.Feature; import com.featurevisor.sdk.VariableSchema; +import java.util.Map; /** * Evaluates not found scenarios and returns appropriate evaluation results @@ -37,19 +38,19 @@ public EvaluateNotFoundResult(Evaluation evaluation, Feature feature, VariableSc /** * Evaluates not found scenarios and returns the appropriate evaluation result * - * @param options The evaluation options containing type, featureKey, variableKey, logger, and datafileReader + * @param options The evaluation options containing type, featureKey, variableKey, diagnostics, and evaluationData * @return EvaluateNotFoundResult containing evaluation, feature, and variableSchema */ public static EvaluateNotFoundResult evaluateNotFound(EvaluateOptions options) { String type = options.getType(); String featureKey = options.getFeatureKey(); String variableKey = options.getVariableKey(); - Logger logger = options.getLogger(); - DatafileReader datafileReader = options.getDatafileReader(); + DiagnosticReporter diagnostics = options.getDiagnostics(); + InstanceEvaluationDataProvider evaluationData = options.getInstanceEvaluationDataProvider(); EvaluateNotFoundResult result = new EvaluateNotFoundResult(); - Feature feature = datafileReader.getFeature(featureKey); + Feature feature = evaluationData.getFeature(featureKey); // feature: not found if (feature == null) { @@ -60,7 +61,7 @@ public static EvaluateNotFoundResult evaluateNotFound(EvaluateOptions options) { result.setEvaluation(evaluation); - logger.warn("feature not found", null); + diagnostics.warn("feature not found", Map.of("evaluation", evaluation)); return result; } @@ -69,7 +70,7 @@ public static EvaluateNotFoundResult evaluateNotFound(EvaluateOptions options) { // feature: deprecated if (Evaluation.TYPE_FLAG.equals(type) && Boolean.TRUE.equals(feature.getDeprecated())) { - logger.warn("feature is deprecated", null); + diagnostics.warn("feature is deprecated", null); } // variableSchema @@ -90,7 +91,7 @@ public static EvaluateNotFoundResult evaluateNotFound(EvaluateOptions options) { result.setEvaluation(evaluation); - logger.warn("variable schema not found", null); + diagnostics.warn("variable schema not found", Map.of("evaluation", evaluation)); return result; } @@ -98,7 +99,7 @@ public static EvaluateNotFoundResult evaluateNotFound(EvaluateOptions options) { result.setVariableSchema(variableSchema); if (Boolean.TRUE.equals(variableSchema.getDeprecated())) { - logger.warn("variable is deprecated", null); + diagnostics.warn("variable is deprecated", null); } } @@ -112,7 +113,7 @@ public static EvaluateNotFoundResult evaluateNotFound(EvaluateOptions options) { result.setEvaluation(evaluation); - logger.warn("no variations", null); + diagnostics.warn("no variations", Map.of("evaluation", evaluation)); return result; } diff --git a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/EvaluateOptions.java b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/EvaluateOptions.java index b933418..8c75128 100644 --- a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/EvaluateOptions.java +++ b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/EvaluateOptions.java @@ -15,15 +15,15 @@ public class EvaluateOptions { // Dependencies private Map context; - private Logger logger; + private DiagnosticReporter diagnostics; private ModulesManager modulesManager; - private DatafileReader datafileReader; + private InstanceEvaluationDataProvider evaluationData; // Override options private Map sticky; private String defaultVariationValue; private Object defaultVariableValue; - private Evaluation flagEvaluation; + private boolean defaultVariableValueSet; // Constructors public EvaluateOptions() {} @@ -44,26 +44,28 @@ public EvaluateOptions(String type, String featureKey, String variableKey) { public String getFeatureKey() { return featureKey; } public String getVariableKey() { return variableKey; } public Map getContext() { return context; } - Logger getLogger() { return logger; } + DiagnosticReporter getDiagnostics() { return diagnostics; } ModulesManager getModulesManager() { return modulesManager; } - DatafileReader getDatafileReader() { return datafileReader; } + InstanceEvaluationDataProvider getInstanceEvaluationDataProvider() { return evaluationData; } public Map getSticky() { return sticky; } public String getDefaultVariationValue() { return defaultVariationValue; } public Object getDefaultVariableValue() { return defaultVariableValue; } - public Evaluation getFlagEvaluation() { return flagEvaluation; } + public boolean hasDefaultVariableValue() { return defaultVariableValueSet; } // Setters public void setType(String type) { this.type = type; } public void setFeatureKey(String featureKey) { this.featureKey = featureKey; } public void setVariableKey(String variableKey) { this.variableKey = variableKey; } public void setContext(Map context) { this.context = context; } - void setLogger(Logger logger) { this.logger = logger; } + void setDiagnostics(DiagnosticReporter diagnostics) { this.diagnostics = diagnostics; } void setModulesManager(ModulesManager modulesManager) { this.modulesManager = modulesManager; } - void setDatafileReader(DatafileReader datafileReader) { this.datafileReader = datafileReader; } + void setInstanceEvaluationDataProvider(InstanceEvaluationDataProvider evaluationData) { this.evaluationData = evaluationData; } public void setSticky(Map sticky) { this.sticky = sticky; } public void setDefaultVariationValue(String defaultVariationValue) { this.defaultVariationValue = defaultVariationValue; } - public void setDefaultVariableValue(Object defaultVariableValue) { this.defaultVariableValue = defaultVariableValue; } - public void setFlagEvaluation(Evaluation flagEvaluation) { this.flagEvaluation = flagEvaluation; } + public void setDefaultVariableValue(Object defaultVariableValue) { + this.defaultVariableValue = defaultVariableValue; + this.defaultVariableValueSet = true; + } // Builder pattern methods public EvaluateOptions type(String type) { @@ -86,8 +88,8 @@ public EvaluateOptions context(Map context) { return this; } - EvaluateOptions logger(Logger logger) { - this.logger = logger; + EvaluateOptions diagnostics(DiagnosticReporter diagnostics) { + this.diagnostics = diagnostics; return this; } @@ -96,8 +98,8 @@ EvaluateOptions modulesManager(ModulesManager modulesManager) { return this; } - EvaluateOptions datafileReader(DatafileReader datafileReader) { - this.datafileReader = datafileReader; + EvaluateOptions evaluationData(InstanceEvaluationDataProvider evaluationData) { + this.evaluationData = evaluationData; return this; } @@ -113,11 +115,13 @@ public EvaluateOptions defaultVariationValue(String defaultVariationValue) { public EvaluateOptions defaultVariableValue(Object defaultVariableValue) { this.defaultVariableValue = defaultVariableValue; + this.defaultVariableValueSet = true; return this; } - public EvaluateOptions flagEvaluation(Evaluation flagEvaluation) { - this.flagEvaluation = flagEvaluation; + EvaluateOptions defaultVariableValue(Object defaultVariableValue, boolean isSet) { + this.defaultVariableValue = defaultVariableValue; + this.defaultVariableValueSet = isSet; return this; } @@ -131,13 +135,13 @@ public EvaluateOptions copy() { copy.featureKey = this.featureKey; copy.variableKey = this.variableKey; copy.context = this.context; - copy.logger = this.logger; + copy.diagnostics = this.diagnostics; copy.modulesManager = this.modulesManager; - copy.datafileReader = this.datafileReader; + copy.evaluationData = this.evaluationData; copy.sticky = this.sticky; copy.defaultVariationValue = this.defaultVariationValue; copy.defaultVariableValue = this.defaultVariableValue; - copy.flagEvaluation = this.flagEvaluation; + copy.defaultVariableValueSet = this.defaultVariableValueSet; return copy; } @@ -159,13 +163,12 @@ public String toString() { ", featureKey='" + featureKey + '\'' + ", variableKey='" + variableKey + '\'' + ", context=" + context + - ", logger=" + logger + + ", diagnostics=" + diagnostics + ", modulesManager=" + modulesManager + - ", datafileReader=" + datafileReader + + ", evaluationData=" + evaluationData + ", sticky=" + sticky + ", defaultVariationValue=" + defaultVariationValue + ", defaultVariableValue=" + defaultVariableValue + - ", flagEvaluation=" + flagEvaluation + '}'; } } diff --git a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/EvaluateSticky.java b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/EvaluateSticky.java index 1547df4..7fb5a6d 100644 --- a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/EvaluateSticky.java +++ b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/EvaluateSticky.java @@ -11,7 +11,7 @@ final class EvaluateSticky { /** * Evaluates sticky scenarios and returns the appropriate evaluation result * - * @param options The evaluation options containing type, featureKey, variableKey, sticky, and logger + * @param options The evaluation options containing type, featureKey, variableKey, sticky, and diagnostics * @return Evaluation if sticky data is found and valid, null otherwise */ public static Evaluation evaluateSticky(EvaluateOptions options) { @@ -19,7 +19,7 @@ public static Evaluation evaluateSticky(EvaluateOptions options) { String featureKey = options.getFeatureKey(); String variableKey = options.getVariableKey(); Map sticky = options.getSticky(); - Logger logger = options.getLogger(); + DiagnosticReporter diagnostics = options.getDiagnostics(); if (sticky != null && sticky.containsKey(featureKey)) { Object stickyData = sticky.get(featureKey); @@ -38,7 +38,7 @@ public static Evaluation evaluateSticky(EvaluateOptions options) { .sticky(stickyMap) .enabled((Boolean) stickyMap.get("enabled")); - logger.debug("using sticky enabled", null); + diagnostics.debug("using sticky enabled", null); return evaluation; } @@ -54,7 +54,7 @@ public static Evaluation evaluateSticky(EvaluateOptions options) { .reason(Evaluation.REASON_STICKY) .variationValue(variationValue.toString()); - logger.debug("using sticky variation", null); + diagnostics.debug("using sticky variation", null); return evaluation; } @@ -79,7 +79,7 @@ public static Evaluation evaluateSticky(EvaluateOptions options) { .variableKey(variableKey) .variableValue(result); - logger.debug("using sticky variable", null); + diagnostics.debug("using sticky variable", null); return evaluation; } diff --git a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/Events.java b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/Events.java index 825c77b..942958e 100644 --- a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/Events.java +++ b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/Events.java @@ -12,7 +12,7 @@ * Event parameter utilities for Featurevisor SDK * Provides methods to generate event details for various SDK events */ -public class Events { +final class Events { /** * Get parameters for sticky set event @@ -21,7 +21,7 @@ public class Events { * @param replace Whether the sticky features were replaced * @return Event details for sticky set event */ - public static Emitter.EventDetails getParamsForStickySetEvent( + public static FeaturevisorEventDetails getParamsForStickySetEvent( Map previousStickyFeatures, Map newStickyFeatures, boolean replace) { @@ -45,7 +45,7 @@ public static Emitter.EventDetails getParamsForStickySetEvent( .distinct() .collect(Collectors.toList()); - Emitter.EventDetails details = new Emitter.EventDetails(); + FeaturevisorEventDetails details = new FeaturevisorEventDetails(); details.put("features", uniqueFeaturesAffected); details.put("replaced", replace); @@ -58,7 +58,7 @@ public static Emitter.EventDetails getParamsForStickySetEvent( * @param newDatafileContent New datafile content * @return Event details for datafile set event */ - public static Emitter.EventDetails getParamsForDatafileSetEvent( + public static FeaturevisorEventDetails getParamsForDatafileSetEvent( DatafileContent previousDatafileContent, DatafileContent newDatafileContent, boolean replace) { @@ -127,7 +127,7 @@ public static Emitter.EventDetails getParamsForDatafileSetEvent( .distinct() .collect(Collectors.toList()); - Emitter.EventDetails details = new Emitter.EventDetails(); + FeaturevisorEventDetails details = new FeaturevisorEventDetails(); details.put("revision", newRevision); details.put("previousRevision", previousRevision); details.put("revisionChanged", !(previousRevision == null ? newRevision == null : previousRevision.equals(newRevision))); diff --git a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/Featurevisor.java b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/Featurevisor.java index f7f17b4..2b415c1 100644 --- a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/Featurevisor.java +++ b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/Featurevisor.java @@ -23,13 +23,13 @@ public class Featurevisor { // from options private Map context = new HashMap<>(); - private Logger logger; + private DiagnosticReporter diagnostics; private Map sticky; private FeaturevisorDiagnosticHandler onDiagnostic; private boolean closed = false; // internally created - private DatafileReader datafileReader; + private InstanceEvaluationDataProvider evaluationData; private ModulesManager modulesManager; private Emitter emitter; private final List moduleDiagnosticSubscriptions = new ArrayList<>(); @@ -148,7 +148,7 @@ public static class OverrideOptions { private Map sticky; private String defaultVariationValue; private Object defaultVariableValue; - private Evaluation flagEvaluation; + private boolean defaultVariableValueSet; public OverrideOptions() {} @@ -156,13 +156,15 @@ public OverrideOptions() {} Map getInternalSticky() { return sticky; } public String getDefaultVariationValue() { return defaultVariationValue; } public Object getDefaultVariableValue() { return defaultVariableValue; } - public Evaluation getFlagEvaluation() { return flagEvaluation; } + public boolean hasDefaultVariableValue() { return defaultVariableValueSet; } // Setters void setInternalSticky(Map sticky) { this.sticky = sticky; } public void setDefaultVariationValue(String defaultVariationValue) { this.defaultVariationValue = defaultVariationValue; } - public void setDefaultVariableValue(Object defaultVariableValue) { this.defaultVariableValue = defaultVariableValue; } - public void setFlagEvaluation(Evaluation flagEvaluation) { this.flagEvaluation = flagEvaluation; } + public void setDefaultVariableValue(Object defaultVariableValue) { + this.defaultVariableValue = defaultVariableValue; + this.defaultVariableValueSet = true; + } // Builder pattern methods public OverrideOptions defaultVariationValue(String defaultVariationValue) { @@ -172,11 +174,7 @@ public OverrideOptions defaultVariationValue(String defaultVariationValue) { public OverrideOptions defaultVariableValue(Object defaultVariableValue) { this.defaultVariableValue = defaultVariableValue; - return this; - } - - public OverrideOptions flagEvaluation(Evaluation flagEvaluation) { - this.flagEvaluation = flagEvaluation; + this.defaultVariableValueSet = true; return this; } } @@ -202,19 +200,34 @@ private Featurevisor(FeaturevisorOptions options) { this.context = new HashMap<>(options.getContext()); } - this.logger = Logger.createLogger(new Logger.CreateLoggerOptions() + this.diagnostics = DiagnosticReporter.createDiagnosticReporter(new DiagnosticReporter.DiagnosticReporterOptions() .level(options.getLogLevel() != null ? options.getLogLevel() : FeaturevisorLogLevel.INFO) .handler((level, message, details) -> { - Map normalizedDetails = details != null ? details : new HashMap<>(); + Map normalizedDetails = details != null ? new HashMap<>(details) : new HashMap<>(); + Object evaluationValue = normalizedDetails.get("evaluation"); + if (evaluationValue instanceof Evaluation) { + Evaluation evaluation = (Evaluation) evaluationValue; + normalizedDetails = new HashMap<>(); + normalizedDetails.put("featureKey", evaluation.getFeatureKey()); + normalizedDetails.put("variableKey", evaluation.getVariableKey()); + normalizedDetails.put("reason", evaluation.getReason()); + normalizedDetails.put("evaluation", evaluation); + } Object reason = normalizedDetails.get("reason"); - String code = reason != null ? reason.toString() : message; + Object explicitCode = normalizedDetails.remove("code"); + Object originalError = normalizedDetails.remove("originalError"); + String code = explicitCode != null ? explicitCode.toString() : (reason != null ? reason.toString() : message); if ("feature is deprecated".equals(message)) code = "deprecated_feature"; if ("variable is deprecated".equals(message)) code = "deprecated_variable"; if ("feature not found".equals(message)) code = "feature_not_found"; if ("variable schema not found".equals(message)) code = "variable_not_found"; if ("no variations".equals(message)) code = "no_variations"; if ("invalid bucketBy".equals(message)) code = "invalid_bucket_by"; - reportDiagnostic(new FeaturevisorDiagnostic(level, code, message).details(normalizedDetails), null); + if ("Error parsing conditions".equals(message)) code = "conditions_parse_error"; + if ("error during evaluation".equals(message)) code = "evaluation_error"; + reportDiagnostic(new FeaturevisorDiagnostic(level, code, message) + .details(normalizedDetails) + .originalError(originalError), null); })); this.emitter = new Emitter(); @@ -222,9 +235,9 @@ private Featurevisor(FeaturevisorOptions options) { this.onDiagnostic = options.getOnDiagnostic(); // datafile - this.datafileReader = new DatafileReader(new DatafileReader.DatafileReaderOptions() + this.evaluationData = new InstanceEvaluationDataProvider(new InstanceEvaluationDataProvider.InstanceEvaluationDataProviderOptions() .datafile(emptyDatafile) - .logger(this.logger)); + .diagnostics(this.diagnostics)); this.modulesManager = new ModulesManager(new ModulesManager.ModulesManagerOptions() .modules(options.getModules() != null ? options.getModules() : new ArrayList<>()) @@ -248,7 +261,7 @@ private Featurevisor(FeaturevisorOptions options) { * Set log level */ public void setLogLevel(FeaturevisorLogLevel level) { - this.logger.setLevel(level); + this.diagnostics.setLevel(level); } private FeaturevisorModuleApi createModuleApi(FeaturevisorModule module) { @@ -359,19 +372,19 @@ void reportDiagnostic(FeaturevisorDiagnostic diagnostic, FeaturevisorModule sour } if (onDiagnostic != null) { - if (shouldReport(diagnostic.getLevel(), this.logger.getLevel())) { + if (shouldReport(diagnostic.getLevel(), this.diagnostics.getLevel())) { try { onDiagnostic.handle(diagnostic); } catch (Throwable error) { System.err.println("[Featurevisor] Diagnostic handler failed: " + error); } } - } else if (shouldReport(diagnostic.getLevel(), this.logger.getLevel())) { - Logger.writeToConsole(diagnostic.getLevel(), diagnostic.getMessage(), diagnosticDetails(diagnostic)); + } else if (shouldReport(diagnostic.getLevel(), this.diagnostics.getLevel())) { + DiagnosticReporter.writeToConsole(diagnostic.getLevel(), diagnostic.getMessage(), diagnosticDetails(diagnostic)); } if (FeaturevisorLogLevel.ERROR.equals(diagnostic.getLevel())) { - this.emitter.trigger(Emitter.EventName.ERROR, new Emitter.EventDetails(Map.of("diagnostic", diagnostic))); + this.emitter.trigger(FeaturevisorEventName.ERROR, new FeaturevisorEventDetails(Map.of("diagnostic", diagnostic))); } } @@ -394,22 +407,22 @@ public void setDatafile(DatafileContent datafile, boolean replace) { datafile.getSegments() == null || datafile.getFeatures() == null) { throw new IllegalArgumentException("Invalid datafile"); } - DatafileContent nextDatafile = replace ? datafile : mergeDatafiles(this.datafileReader.getDatafile(), datafile); - DatafileReader newDatafileReader = new DatafileReader(new DatafileReader.DatafileReaderOptions() + DatafileContent nextDatafile = replace ? datafile : mergeDatafiles(this.evaluationData.getDatafile(), datafile); + InstanceEvaluationDataProvider newInstanceEvaluationDataProvider = new InstanceEvaluationDataProvider(new InstanceEvaluationDataProvider.InstanceEvaluationDataProviderOptions() .datafile(nextDatafile) - .logger(this.logger)); + .diagnostics(this.diagnostics)); - Emitter.EventDetails details = Events.getParamsForDatafileSetEvent( - this.datafileReader.getDatafile(), newDatafileReader.getDatafile(), replace); + FeaturevisorEventDetails details = Events.getParamsForDatafileSetEvent( + this.evaluationData.getDatafile(), newInstanceEvaluationDataProvider.getDatafile(), replace); - this.datafileReader = newDatafileReader; + this.evaluationData = newInstanceEvaluationDataProvider; reportDiagnostic(new FeaturevisorDiagnostic() .level(FeaturevisorLogLevel.INFO) .code("datafile_set") .message("Datafile set") .details(details), null); - this.emitter.trigger(Emitter.EventName.DATAFILE_SET, details); + this.emitter.trigger(FeaturevisorEventName.DATAFILE_SET, details); } catch (Exception e) { reportDiagnostic(new FeaturevisorDiagnostic() .level(FeaturevisorLogLevel.ERROR) @@ -476,6 +489,10 @@ private DatafileContent mergeDatafiles(DatafileContent previous, DatafileContent /** * Set sticky features */ + public void setSticky(Map sticky) { + setSticky(sticky, false); + } + public void setSticky(Map sticky, boolean replace) { Map previousStickyFeatures = this.sticky != null ? new HashMap<>(this.sticky) : new HashMap<>(); @@ -487,7 +504,7 @@ public void setSticky(Map sticky, boolean replace) { this.sticky.putAll(sticky); } - Emitter.EventDetails params = Events.getParamsForStickySetEvent( + FeaturevisorEventDetails params = Events.getParamsForStickySetEvent( previousStickyFeatures, this.sticky, replace); reportDiagnostic(new FeaturevisorDiagnostic() @@ -495,41 +512,41 @@ public void setSticky(Map sticky, boolean replace) { .code("sticky_set") .message("Sticky features set") .details(params), null); - this.emitter.trigger(Emitter.EventName.STICKY_SET, params); + this.emitter.trigger(FeaturevisorEventName.STICKY_SET, params); } /** * Get revision */ public String getRevision() { - return this.datafileReader.getRevision(); + return this.evaluationData.getRevision(); } public String getSchemaVersion() { - return this.datafileReader.getSchemaVersion(); + return this.evaluationData.getSchemaVersion(); } public Segment getSegment(String segmentKey) { - return this.datafileReader.getSegment(segmentKey); + return this.evaluationData.getSegment(segmentKey); } public List getFeatureKeys() { - return this.datafileReader.getFeatureKeys(); + return this.evaluationData.getFeatureKeys(); } public List getVariableKeys(String featureKey) { - return this.datafileReader.getVariableKeys(featureKey); + return this.evaluationData.getVariableKeys(featureKey); } public boolean hasVariations(String featureKey) { - return this.datafileReader.hasVariations(featureKey); + return this.evaluationData.hasVariations(featureKey); } /** * Get feature */ public Feature getFeature(String featureKey) { - return this.datafileReader.getFeature(featureKey); + return this.evaluationData.getFeature(featureKey); } /** @@ -546,7 +563,7 @@ public void removeModule(String name) { /** * Subscribe to event */ - public Emitter.UnsubscribeFunction on(Emitter.EventName eventName, Emitter.EventCallback callback) { + public FeaturevisorUnsubscribe on(FeaturevisorEventName eventName, FeaturevisorEventHandler callback) { return this.emitter.on(eventName, callback); } @@ -563,6 +580,10 @@ public void close() { /** * Context */ + public void setContext(Map context) { + setContext(context, false); + } + public void setContext(Map context, boolean replace) { if (replace) { this.context = new HashMap<>(context); @@ -571,11 +592,11 @@ public void setContext(Map context, boolean replace) { this.context.putAll(context); } - Emitter.EventDetails eventDetails = new Emitter.EventDetails(); + FeaturevisorEventDetails eventDetails = new FeaturevisorEventDetails(); eventDetails.put("context", this.context); eventDetails.put("replaced", replace); - this.emitter.trigger(Emitter.EventName.CONTEXT_SET, eventDetails); + this.emitter.trigger(FeaturevisorEventName.CONTEXT_SET, eventDetails); reportDiagnostic(new FeaturevisorDiagnostic() .level(FeaturevisorLogLevel.DEBUG) .code("context_set") @@ -633,13 +654,12 @@ private EvaluateOptions getEvaluationDependencies(Map context, O return new EvaluateOptions() .context(getContext(context)) - .logger(this.logger) + .diagnostics(this.diagnostics) .modulesManager(this.modulesManager) - .datafileReader(this.datafileReader) + .evaluationData(this.evaluationData) .sticky(mergedSticky) .defaultVariationValue(options.getDefaultVariationValue()) - .defaultVariableValue(options.getDefaultVariableValue()) - .flagEvaluation(options.getFlagEvaluation()); + .defaultVariableValue(options.getDefaultVariableValue(), options.hasDefaultVariableValue()); } public Evaluation evaluateFlag(String featureKey, Map context, OverrideOptions options) { @@ -663,7 +683,7 @@ public boolean isEnabled(String featureKey, Map context, Overrid Evaluation evaluation = evaluateFlag(featureKey, context, options); return Boolean.TRUE.equals(evaluation.getEnabled()); } catch (Exception e) { - this.logger.error("isEnabled", Map.of("featureKey", featureKey, "error", e.getMessage())); + this.diagnostics.error("isEnabled", Map.of("featureKey", featureKey, "error", e.getMessage())); return false; } } @@ -709,7 +729,7 @@ public String getVariation(String featureKey, Map context, Overr return null; } catch (Exception e) { - this.logger.error("getVariation", Map.of("featureKey", featureKey, "error", e.getMessage())); + this.diagnostics.error("getVariation", Map.of("featureKey", featureKey, "error", e.getMessage())); return null; } } @@ -765,7 +785,7 @@ public Object getVariable(String featureKey, String variableKey, Map context, List result = new HashMap<>(); - List keys = featureKeys.isEmpty() ? this.datafileReader.getFeatureKeys() : featureKeys; + List keys = featureKeys.isEmpty() ? this.evaluationData.getFeatureKeys() : featureKeys; for (String featureKey : keys) { // isEnabled Evaluation flagEvaluation = evaluateFlag(featureKey, context, options); @@ -1053,13 +1073,14 @@ public EvaluatedFeatures getAllEvaluations(Map context, List context, List variableKeys = this.datafileReader.getVariableKeys(featureKey); + List variableKeys = this.evaluationData.getVariableKeys(featureKey); if (!variableKeys.isEmpty()) { Map variables = new HashMap<>(); diff --git a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/FeaturevisorEventDetails.java b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/FeaturevisorEventDetails.java new file mode 100644 index 0000000..0ddb9ef --- /dev/null +++ b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/FeaturevisorEventDetails.java @@ -0,0 +1,14 @@ +package com.featurevisor.sdk; + +import java.util.HashMap; +import java.util.Map; + +public final class FeaturevisorEventDetails extends HashMap { + public FeaturevisorEventDetails() { + super(); + } + + public FeaturevisorEventDetails(Map map) { + super(map); + } +} diff --git a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/FeaturevisorEventHandler.java b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/FeaturevisorEventHandler.java new file mode 100644 index 0000000..1e2514f --- /dev/null +++ b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/FeaturevisorEventHandler.java @@ -0,0 +1,6 @@ +package com.featurevisor.sdk; + +@FunctionalInterface +public interface FeaturevisorEventHandler { + void handle(FeaturevisorEventDetails details); +} diff --git a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/FeaturevisorEventName.java b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/FeaturevisorEventName.java new file mode 100644 index 0000000..867f1e8 --- /dev/null +++ b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/FeaturevisorEventName.java @@ -0,0 +1,27 @@ +package com.featurevisor.sdk; + +public enum FeaturevisorEventName { + DATAFILE_SET("datafile_set"), + CONTEXT_SET("context_set"), + STICKY_SET("sticky_set"), + ERROR("error"); + + private final String value; + + FeaturevisorEventName(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static FeaturevisorEventName fromString(String value) { + for (FeaturevisorEventName eventName : values()) { + if (eventName.value.equals(value)) { + return eventName; + } + } + throw new IllegalArgumentException("Unknown event name: " + value); + } +} diff --git a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/FeaturevisorModule.java b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/FeaturevisorModule.java index ef69ae8..04bb175 100644 --- a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/FeaturevisorModule.java +++ b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/FeaturevisorModule.java @@ -10,8 +10,8 @@ public class FeaturevisorModule { private String name; private Consumer setup; private Function before; - private ModulesManager.ConfigureBucketKey bucketKey; - private ModulesManager.ConfigureBucketValue bucketValue; + private ConfigureBucketKey bucketKey; + private ConfigureBucketValue bucketValue; private BiFunction after; private Runnable close; @@ -23,16 +23,16 @@ public FeaturevisorModule(String name) { public String getName() { return name; } public Consumer getSetup() { return setup; } public Function getBefore() { return before; } - public ModulesManager.ConfigureBucketKey getBucketKey() { return bucketKey; } - public ModulesManager.ConfigureBucketValue getBucketValue() { return bucketValue; } + public ConfigureBucketKey getBucketKey() { return bucketKey; } + public ConfigureBucketValue getBucketValue() { return bucketValue; } public BiFunction getAfter() { return after; } public Runnable getClose() { return close; } public void setName(String name) { this.name = name; } public void setSetup(Consumer setup) { this.setup = setup; } public void setBefore(Function before) { this.before = before; } - public void setBucketKey(ModulesManager.ConfigureBucketKey bucketKey) { this.bucketKey = bucketKey; } - public void setBucketValue(ModulesManager.ConfigureBucketValue bucketValue) { this.bucketValue = bucketValue; } + public void setBucketKey(ConfigureBucketKey bucketKey) { this.bucketKey = bucketKey; } + public void setBucketValue(ConfigureBucketValue bucketValue) { this.bucketValue = bucketValue; } public void setAfter(BiFunction after) { this.after = after; } public void setClose(Runnable close) { this.close = close; } @@ -46,12 +46,12 @@ public FeaturevisorModule before(Function befo return this; } - public FeaturevisorModule bucketKey(ModulesManager.ConfigureBucketKey bucketKey) { + public FeaturevisorModule bucketKey(ConfigureBucketKey bucketKey) { this.bucketKey = bucketKey; return this; } - public FeaturevisorModule bucketValue(ModulesManager.ConfigureBucketValue bucketValue) { + public FeaturevisorModule bucketValue(ConfigureBucketValue bucketValue) { this.bucketValue = bucketValue; return this; } diff --git a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/FeaturevisorUnsubscribe.java b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/FeaturevisorUnsubscribe.java new file mode 100644 index 0000000..b253989 --- /dev/null +++ b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/FeaturevisorUnsubscribe.java @@ -0,0 +1,6 @@ +package com.featurevisor.sdk; + +@FunctionalInterface +public interface FeaturevisorUnsubscribe { + void unsubscribe(); +} diff --git a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/DatafileReader.java b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/InstanceEvaluationDataProvider.java similarity index 89% rename from featurevisor-sdk/src/main/java/com/featurevisor/sdk/DatafileReader.java rename to featurevisor-sdk/src/main/java/com/featurevisor/sdk/InstanceEvaluationDataProvider.java index 6a5e6fb..717b172 100644 --- a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/DatafileReader.java +++ b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/InstanceEvaluationDataProvider.java @@ -15,36 +15,36 @@ import java.util.List; import java.util.ArrayList; import java.util.regex.Pattern; -import java.util.regex.PatternSyntaxException; +import java.util.concurrent.ConcurrentHashMap; /** - * DatafileReader for Featurevisor SDK + * InstanceEvaluationDataProvider for Featurevisor SDK * Handles reading and parsing datafile content */ -class DatafileReader { +class InstanceEvaluationDataProvider { /** - * Options for creating a DatafileReader + * Options for creating a InstanceEvaluationDataProvider */ - public static class DatafileReaderOptions { + public static class InstanceEvaluationDataProviderOptions { private DatafileContent datafile; - private Logger logger; + private DiagnosticReporter diagnostics; - public DatafileReaderOptions() {} + public InstanceEvaluationDataProviderOptions() {} - public DatafileReaderOptions datafile(DatafileContent datafile) { + public InstanceEvaluationDataProviderOptions datafile(DatafileContent datafile) { this.datafile = datafile; return this; } - public DatafileReaderOptions logger(Logger logger) { - this.logger = logger; + public InstanceEvaluationDataProviderOptions diagnostics(DiagnosticReporter diagnostics) { + this.diagnostics = diagnostics; return this; } // Getters public DatafileContent getDatafile() { return datafile; } - public Logger getLogger() { return logger; } + public DiagnosticReporter getDiagnostics() { return diagnostics; } } /** @@ -73,14 +73,14 @@ public ForceResult(Force force, Integer forceIndex) { private String featurevisorVersion; private Map segments; private Map features; - private Logger logger; + private DiagnosticReporter diagnostics; // Cache for regex patterns to avoid creating new objects for the same regex private Map regexCache; - public DatafileReader(DatafileReaderOptions options) { + public InstanceEvaluationDataProvider(InstanceEvaluationDataProviderOptions options) { DatafileContent datafile = options.getDatafile(); - this.logger = options.getLogger(); + this.diagnostics = options.getDiagnostics(); this.schemaVersion = datafile.getSchemaVersion(); this.revision = datafile.getRevision(); @@ -93,7 +93,7 @@ public DatafileReader(DatafileReaderOptions options) { if (this.features == null) { this.features = new HashMap<>(); } - this.regexCache = new HashMap<>(); + this.regexCache = new ConcurrentHashMap<>(); } public String getRevision() { @@ -166,9 +166,8 @@ public Pattern getRegex(String regexString, String regexFlags) { Pattern regex = Pattern.compile(regexString, getPatternFlags(flags)); regexCache.put(cacheKey, regex); return regex; - } catch (PatternSyntaxException e) { - logger.error("Invalid regex pattern: " + regexString, null); - return null; + } catch (IllegalArgumentException e) { + throw e; } } @@ -178,7 +177,11 @@ private int getPatternFlags(String flags) { if (flags.contains("m")) patternFlags |= Pattern.MULTILINE; if (flags.contains("s")) patternFlags |= Pattern.DOTALL; if (flags.contains("u")) patternFlags |= Pattern.UNICODE_CASE; - if (flags.contains("x")) patternFlags |= Pattern.COMMENTS; + for (char flag : flags.toCharArray()) { + if ("gimsuy".indexOf(flag) == -1) { + throw new IllegalArgumentException("Invalid regular expression flag: " + flag); + } + } return patternFlags; } @@ -200,10 +203,11 @@ public boolean allConditionsAreMatched(Object conditions, Map co return Conditions.conditionIsMatched(condition, context, getRegex); } catch (Exception e) { Map details = new HashMap<>(); - details.put("error", e); + details.put("code", "condition_match_error"); + details.put("originalError", e); details.put("condition", condition); details.put("context", context); - logger.warn(e.getMessage(), details); + diagnostics.warn(e.getMessage(), details); return false; } } @@ -235,10 +239,11 @@ public boolean allConditionsAreMatched(Object conditions, Map co return Conditions.conditionIsMatched(condition, context, getRegex); } catch (Exception e) { Map details = new HashMap<>(); - details.put("error", e); + details.put("code", "condition_match_error"); + details.put("originalError", e); details.put("condition", conditionMap); details.put("context", context); - logger.warn(e.getMessage(), details); + diagnostics.warn(e.getMessage(), details); return false; } } @@ -423,9 +428,10 @@ public Object parseConditionsIfStringified(Object conditions) { return mapper.readValue(conditionsStr, Object.class); } catch (Exception e) { Map details = new HashMap<>(); - details.put("error", e); + details.put("code", "conditions_parse_error"); + details.put("originalError", e); details.put("conditions", conditions); - logger.error("Error parsing conditions", details); + diagnostics.error("Error parsing conditions", details); return conditions; } } @@ -438,8 +444,7 @@ public Object parseSegmentsIfStringified(Object segments) { ObjectMapper mapper = new ObjectMapper(); return mapper.readValue(segmentsStr, Object.class); } catch (Exception e) { - logger.error("Error parsing segments: " + segmentsStr, null); - return segments; + throw new IllegalArgumentException("Error parsing segments", e); } } } diff --git a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/ModulesManager.java b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/ModulesManager.java index 5d80b25..0a460a0 100644 --- a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/ModulesManager.java +++ b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/ModulesManager.java @@ -2,73 +2,14 @@ import java.util.ArrayList; import java.util.List; -import java.util.Map; -public class ModulesManager { +final class ModulesManager { private final List modules = new ArrayList<>(); private final FeaturevisorDiagnosticReporter diagnosticReporter; private final FeaturevisorModuleApiFactory moduleApiFactory; private final java.util.function.Consumer clearModuleDiagnosticSubscriptions; - public static class ConfigureBucketKeyOptions { - private String featureKey; - private Map context; - private Bucket bucketBy; - private String bucketKey; - - public ConfigureBucketKeyOptions(String featureKey, Map context, Bucket bucketBy, String bucketKey) { - this.featureKey = featureKey; - this.context = context; - this.bucketBy = bucketBy; - this.bucketKey = bucketKey; - } - - public String getFeatureKey() { return featureKey; } - public Map getContext() { return context; } - public Bucket getBucketBy() { return bucketBy; } - public String getBucketKey() { return bucketKey; } - - public void setFeatureKey(String featureKey) { this.featureKey = featureKey; } - public void setContext(Map context) { this.context = context; } - public void setBucketBy(Bucket bucketBy) { this.bucketBy = bucketBy; } - public void setBucketKey(String bucketKey) { this.bucketKey = bucketKey; } - } - - public static class ConfigureBucketValueOptions { - private String featureKey; - private String bucketKey; - private Map context; - private int bucketValue; - - public ConfigureBucketValueOptions(String featureKey, String bucketKey, Map context, int bucketValue) { - this.featureKey = featureKey; - this.bucketKey = bucketKey; - this.context = context; - this.bucketValue = bucketValue; - } - - public String getFeatureKey() { return featureKey; } - public String getBucketKey() { return bucketKey; } - public Map getContext() { return context; } - public int getBucketValue() { return bucketValue; } - - public void setFeatureKey(String featureKey) { this.featureKey = featureKey; } - public void setBucketKey(String bucketKey) { this.bucketKey = bucketKey; } - public void setContext(Map context) { this.context = context; } - public void setBucketValue(int bucketValue) { this.bucketValue = bucketValue; } - } - - @FunctionalInterface - public interface ConfigureBucketKey { - String configure(ConfigureBucketKeyOptions options); - } - - @FunctionalInterface - public interface ConfigureBucketValue { - int configure(ConfigureBucketValueOptions options); - } - - public static class ModulesManagerOptions { + static class ModulesManagerOptions { private List modules; private FeaturevisorDiagnosticReporter diagnosticReporter; private FeaturevisorModuleApiFactory moduleApiFactory; diff --git a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/MurmurHash.java b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/MurmurHash.java index 8794861..00e8ecc 100644 --- a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/MurmurHash.java +++ b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/MurmurHash.java @@ -6,7 +6,7 @@ * MurmurHash v3 implementation for Featurevisor SDK * Based on the TypeScript implementation from: https://github.com/perezd/node-murmurhash */ -public class MurmurHash { +final class MurmurHash { /** * MurmurHash v3 implementation diff --git a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/Operator.java b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/Operator.java index 25e6d50..60069cf 100644 --- a/featurevisor-sdk/src/main/java/com/featurevisor/sdk/Operator.java +++ b/featurevisor-sdk/src/main/java/com/featurevisor/sdk/Operator.java @@ -1,5 +1,8 @@ package com.featurevisor.sdk; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + public enum Operator { EQUALS("equals"), NOT_EQUALS("notEquals"), @@ -48,7 +51,19 @@ public enum Operator { this.value = value; } + @JsonValue public String getValue() { return value; } + + @JsonCreator + public static Operator fromValue(String value) { + for (Operator operator : values()) { + if (operator.value.equals(value)) { + return operator; + } + } + + throw new IllegalArgumentException("Unknown condition operator: " + value); + } } diff --git a/featurevisor-sdk/src/test/java/com/featurevisor/sdk/BucketerTest.java b/featurevisor-sdk/src/test/java/com/featurevisor/sdk/BucketerTest.java index a8c5d65..c606648 100644 --- a/featurevisor-sdk/src/test/java/com/featurevisor/sdk/BucketerTest.java +++ b/featurevisor-sdk/src/test/java/com/featurevisor/sdk/BucketerTest.java @@ -12,11 +12,11 @@ public class BucketerTest { - private Logger logger; + private DiagnosticReporter diagnostics; @BeforeEach public void setUp() { - logger = Logger.createLogger(new Logger.CreateLoggerOptions().level(FeaturevisorLogLevel.WARN)); + diagnostics = DiagnosticReporter.createDiagnosticReporter(new DiagnosticReporter.DiagnosticReporterOptions().level(FeaturevisorLogLevel.WARN)); } @Test @@ -76,7 +76,7 @@ public void testPlainBucketByReturnsBucketKey() { .featureKey(featureKey) .bucketBy(bucketBy) .context(context) - .logger(logger)); + .diagnostics(diagnostics)); assertEquals("123.test-feature", bucketKey); } @@ -92,11 +92,45 @@ public void testPlainBucketByReturnsFeatureKeyOnlyIfValueMissingInContext() { .featureKey(featureKey) .bucketBy(bucketBy) .context(context) - .logger(logger)); + .diagnostics(diagnostics)); assertEquals("test-feature", bucketKey); } + @Test + public void testJavaScriptStringificationKeepsExplicitNullAndCollections() { + Map context = new HashMap<>(); + context.put("empty", null); + context.put("enabled", false); + context.put("values", Arrays.asList(1, true, null)); + context.put("object", Map.of("id", 1)); + + String bucketKey = Bucketer.getBucketKey(new Bucketer.GetBucketKeyOptions() + .featureKey("feature") + .bucketBy(new Bucket(Arrays.asList("empty", "enabled", "values", "object"), true)) + .context(context) + .diagnostics(diagnostics)); + + assertEquals(".false.1,true,.[object Object].feature", bucketKey); + } + + @Test + public void testJavaScriptStringificationNormalizesWholeDoublesAndNegativeZero() { + Map context = new HashMap<>(); + context.put("whole", 1.0); + context.put("negativeZero", -0.0); + context.put("small", 1e-6); + context.put("large", 1e21); + + String bucketKey = Bucketer.getBucketKey(new Bucketer.GetBucketKeyOptions() + .featureKey("feature") + .bucketBy(new Bucket(Arrays.asList("whole", "negativeZero", "small", "large"), true)) + .context(context) + .diagnostics(diagnostics)); + + assertEquals("1.0.0.000001.1e+21.feature", bucketKey); + } + @Test public void testAndBucketByCombinesMultipleFieldValuesTogetherIfPresent() { String featureKey = "test-feature"; @@ -111,7 +145,7 @@ public void testAndBucketByCombinesMultipleFieldValuesTogetherIfPresent() { .featureKey(featureKey) .bucketBy(bucketBy) .context(context) - .logger(logger)); + .diagnostics(diagnostics)); assertEquals("123.234.test-feature", bucketKey); } @@ -129,7 +163,7 @@ public void testAndBucketByCombinesOnlyAvailableFieldValuesTogetherIfPresent() { .featureKey(featureKey) .bucketBy(bucketBy) .context(context) - .logger(logger)); + .diagnostics(diagnostics)); assertEquals("123.test-feature", bucketKey); } @@ -151,7 +185,7 @@ public void testAndBucketByCombinesAllAvailableFieldsWithDotSeparatedPaths() { .featureKey(featureKey) .bucketBy(bucketBy) .context(context) - .logger(logger)); + .diagnostics(diagnostics)); assertEquals("123.234.test-feature", bucketKey); } @@ -172,7 +206,7 @@ public void testOrBucketByTakesFirstAvailableFieldValue() { .featureKey(featureKey) .bucketBy(bucketBy) .context(context) - .logger(logger)); + .diagnostics(diagnostics)); assertEquals("234.test-feature", bucketKey); } @@ -192,7 +226,7 @@ public void testOrBucketByTakesFirstAvailableFieldValueWhenFirstIsMissing() { .featureKey(featureKey) .bucketBy(bucketBy) .context(context) - .logger(logger)); + .diagnostics(diagnostics)); assertEquals("deviceIdHere.test-feature", bucketKey); } @@ -208,7 +242,7 @@ public void testInvalidBucketByThrowsException() { .featureKey(featureKey) .bucketBy(bucketBy) .context(context) - .logger(logger)); + .diagnostics(diagnostics)); }); } } diff --git a/featurevisor-sdk/src/test/java/com/featurevisor/sdk/ChildTest.java b/featurevisor-sdk/src/test/java/com/featurevisor/sdk/ChildTest.java index e85d5d3..b620f9b 100644 --- a/featurevisor-sdk/src/test/java/com/featurevisor/sdk/ChildTest.java +++ b/featurevisor-sdk/src/test/java/com/featurevisor/sdk/ChildTest.java @@ -23,15 +23,16 @@ import java.util.List; import java.util.ArrayList; import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; import com.fasterxml.jackson.databind.ObjectMapper; public class ChildTest { - private Logger logger; + private DiagnosticReporter diagnostics; @BeforeEach public void setUp() { - logger = Logger.createLogger(new Logger.CreateLoggerOptions().level(FeaturevisorLogLevel.WARN)); + diagnostics = DiagnosticReporter.createDiagnosticReporter(new DiagnosticReporter.DiagnosticReporterOptions().level(FeaturevisorLogLevel.WARN)); } @Test @@ -284,10 +285,13 @@ public void testCreateChildInstance() { // Test feature evaluation assertTrue(childInstance.isEnabled("test")); assertEquals("control", childInstance.getVariation("test")); + assertEquals(true, childInstance.evaluateFlag("test").getEnabled()); + assertEquals("control", childInstance.evaluateVariation("test").getVariation().getValue()); // Test variable retrieval assertEquals("black", childInstance.getVariable("test", "color")); assertEquals("black", childInstance.getVariableString("test", "color")); + assertEquals("black", childInstance.evaluateVariable("test", "color").getVariableValue()); assertEquals(false, childInstance.getVariable("test", "showSidebar")); assertEquals(false, childInstance.getVariableBoolean("test", "showSidebar")); @@ -322,6 +326,7 @@ public void testCreateChildInstance() { stickyFeature.put("enabled", true); childInstance.setSticky(Map.of("newFeature", stickyFeature), false); assertTrue(childInstance.isEnabled("newFeature")); + assertEquals("sticky", childInstance.evaluateFlag("newFeature").getReason()); // Test getAllEvaluations com.featurevisor.sdk.EvaluatedFeatures allEvaluations = childInstance.getAllEvaluations(); @@ -332,4 +337,41 @@ public void testCreateChildInstance() { // Test close childInstance.close(); } + + @Test + public void testCloseRemovesDelegatedParentSubscriptions() { + Featurevisor parent = Featurevisor.createFeaturevisor( + new Featurevisor.FeaturevisorOptions().logLevel(FeaturevisorLogLevel.FATAL) + ); + ChildInstance child = parent.spawn(); + AtomicInteger calls = new AtomicInteger(); + child.on(FeaturevisorEventName.DATAFILE_SET, details -> calls.incrementAndGet()); + + child.close(); + child.close(); + DatafileContent datafile = new DatafileContent(); + datafile.setSchemaVersion("2"); + datafile.setRevision("after-close"); + datafile.setSegments(new HashMap<>()); + datafile.setFeatures(new HashMap<>()); + parent.setDatafile(datafile, true); + + assertEquals(0, calls.get()); + } + + @Test + public void testContextMatchesJavaScriptSnapshotBehavior() { + Featurevisor parent = Featurevisor.createFeaturevisor( + new Featurevisor.FeaturevisorOptions() + .context(new HashMap<>(Map.of("country", "nl", "plan", "free"))) + .logLevel(FeaturevisorLogLevel.FATAL) + ); + ChildInstance child = parent.spawn(new HashMap<>(Map.of("country", "de"))); + parent.setContext(new HashMap<>(Map.of("plan", "pro", "locale", "de-DE")), false); + + assertEquals( + Map.of("country", "de", "plan", "free", "locale", "de-DE"), + child.getContext() + ); + } } diff --git a/featurevisor-sdk/src/test/java/com/featurevisor/sdk/ConditionsTest.java b/featurevisor-sdk/src/test/java/com/featurevisor/sdk/ConditionsTest.java index 4e345e7..42c1070 100644 --- a/featurevisor-sdk/src/test/java/com/featurevisor/sdk/ConditionsTest.java +++ b/featurevisor-sdk/src/test/java/com/featurevisor/sdk/ConditionsTest.java @@ -15,12 +15,12 @@ public class ConditionsTest { - private Logger logger; - private DatafileReader datafileReader; + private DiagnosticReporter diagnostics; + private InstanceEvaluationDataProvider evaluationData; @BeforeEach public void setUp() { - logger = Logger.createLogger(new Logger.CreateLoggerOptions().level(FeaturevisorLogLevel.WARN)); + diagnostics = DiagnosticReporter.createDiagnosticReporter(new DiagnosticReporter.DiagnosticReporterOptions().level(FeaturevisorLogLevel.WARN)); DatafileContent datafile = new DatafileContent(); datafile.setSchemaVersion("2.0"); @@ -28,26 +28,69 @@ public void setUp() { datafile.setSegments(new HashMap<>()); datafile.setFeatures(new HashMap<>()); - datafileReader = new DatafileReader(new DatafileReader.DatafileReaderOptions() + evaluationData = new InstanceEvaluationDataProvider(new InstanceEvaluationDataProvider.InstanceEvaluationDataProviderOptions() .datafile(datafile) - .logger(logger)); + .diagnostics(diagnostics)); } @Test public void testConditionsIsFunction() { // Test that the method exists and can be called - assertTrue(datafileReader.allConditionsAreMatched("*", new HashMap<>())); + assertTrue(evaluationData.allConditionsAreMatched("*", new HashMap<>())); } @Test public void testMatchAllViaWildcard() { // match String conditions = "*"; - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("browser_type", "chrome"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("browser_type", "chrome"))); // not match String conditions2 = "blah"; - assertFalse(datafileReader.allConditionsAreMatched(conditions2, Map.of("browser_type", "chrome"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions2, Map.of("browser_type", "chrome"))); + } + + @Test + public void testJavaScriptPrimitiveAndPresenceSemantics() { + Condition equalsNumber = new Condition(); + equalsNumber.setAttribute("value"); + equalsNumber.setOperator(Operator.EQUALS); + equalsNumber.setValue(1); + + assertFalse(evaluationData.allConditionsAreMatched(equalsNumber, Map.of("value", "1"))); + assertFalse(evaluationData.allConditionsAreMatched(equalsNumber, Map.of("value", true))); + assertTrue(evaluationData.allConditionsAreMatched(equalsNumber, Map.of("value", 1.0))); + assertTrue(evaluationData.allConditionsAreMatched( + condition("missing", Operator.NOT_EQUALS, "value"), Map.of())); + assertTrue(evaluationData.allConditionsAreMatched( + condition("missing", Operator.NOT_EQUALS, null), Map.of())); + + Map explicitNull = new HashMap<>(); + explicitNull.put("value", null); + assertTrue(evaluationData.allConditionsAreMatched(condition("value", Operator.EXISTS, null), explicitNull)); + assertFalse(evaluationData.allConditionsAreMatched(condition("value", Operator.NOT_EXISTS, null), explicitNull)); + assertFalse(evaluationData.allConditionsAreMatched(condition("missing", Operator.IN, Arrays.asList((Object) null)), Map.of())); + } + + @Test + public void testIncludesAllPrimitivesAndRegexSearchSemantics() { + Map context = new HashMap<>(); + context.put("values", Arrays.asList(1, true, null)); + context.put("text", "prefix-value-suffix"); + + assertTrue(evaluationData.allConditionsAreMatched(condition("values", Operator.INCLUDES, 1.0), context)); + assertTrue(evaluationData.allConditionsAreMatched(condition("values", Operator.INCLUDES, true), context)); + assertTrue(evaluationData.allConditionsAreMatched(condition("values", Operator.INCLUDES, null), context)); + assertTrue(evaluationData.allConditionsAreMatched(condition("text", Operator.MATCHES, "value"), context)); + assertFalse(evaluationData.allConditionsAreMatched(condition("text", Operator.NOT_INCLUDES, "value"), context)); + } + + private Condition condition(String attribute, Operator operator, Object value) { + Condition condition = new Condition(); + condition.setAttribute(attribute); + condition.setOperator(operator); + condition.setValue(value); + return condition; } @Test @@ -60,10 +103,10 @@ public void testOperatorEquals() { conditions.add(condition); // match - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("browser_type", "chrome"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("browser_type", "chrome"))); // not match - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("browser_type", "firefox"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("browser_type", "firefox"))); } @Test @@ -80,19 +123,19 @@ public void testOperatorEqualsWithDotSeparatedPath() { Map browser = new HashMap<>(); browser.put("type", "chrome"); context.put("browser", browser); - assertTrue(datafileReader.allConditionsAreMatched(conditions, context)); + assertTrue(evaluationData.allConditionsAreMatched(conditions, context)); // not match browser.put("type", "firefox"); - assertFalse(datafileReader.allConditionsAreMatched(conditions, context)); + assertFalse(evaluationData.allConditionsAreMatched(conditions, context)); browser.put("blah", "firefox"); browser.remove("type"); - assertFalse(datafileReader.allConditionsAreMatched(conditions, context)); + assertFalse(evaluationData.allConditionsAreMatched(conditions, context)); context.clear(); context.put("browser", "firefox"); - assertFalse(datafileReader.allConditionsAreMatched(conditions, context)); + assertFalse(evaluationData.allConditionsAreMatched(conditions, context)); } @Test @@ -105,10 +148,10 @@ public void testOperatorNotEquals() { conditions.add(condition); // match - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("browser_type", "firefox"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("browser_type", "firefox"))); // not match - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("browser_type", "chrome"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("browser_type", "chrome"))); } @Test @@ -120,10 +163,10 @@ public void testOperatorExists() { conditions.add(condition); // match - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("browser_type", "firefox"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("browser_type", "firefox"))); // not match - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("not_browser_type", "chrome"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("not_browser_type", "chrome"))); } @Test @@ -139,21 +182,21 @@ public void testOperatorExistsWithDotSeparatedPath() { Map browser = new HashMap<>(); browser.put("name", "chrome"); context.put("browser", browser); - assertTrue(datafileReader.allConditionsAreMatched(conditions, context)); + assertTrue(evaluationData.allConditionsAreMatched(conditions, context)); // not match context.clear(); context.put("browser", "chrome"); - assertFalse(datafileReader.allConditionsAreMatched(conditions, context)); + assertFalse(evaluationData.allConditionsAreMatched(conditions, context)); browser.clear(); browser.put("version", "1.2.3"); context.put("browser", browser); - assertFalse(datafileReader.allConditionsAreMatched(conditions, context)); + assertFalse(evaluationData.allConditionsAreMatched(conditions, context)); context.clear(); context.put("version", "1.2.3"); - assertFalse(datafileReader.allConditionsAreMatched(conditions, context)); + assertFalse(evaluationData.allConditionsAreMatched(conditions, context)); } @Test @@ -165,11 +208,11 @@ public void testOperatorNotExists() { conditions.add(condition); // match - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("not_name", "Hello World"))); - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("not_name", "Hello Universe"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("not_name", "Hello World"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("not_name", "Hello Universe"))); // not match - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("name", "Hi World"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("name", "Hi World"))); } @Test @@ -185,17 +228,17 @@ public void testOperatorNotExistsWithDotSeparatedPath() { Map browser = new HashMap<>(); browser.put("not_name", "Hello World"); context.put("browser", browser); - assertTrue(datafileReader.allConditionsAreMatched(conditions, context)); + assertTrue(evaluationData.allConditionsAreMatched(conditions, context)); context.clear(); context.put("not_name", "Hello Universe"); - assertTrue(datafileReader.allConditionsAreMatched(conditions, context)); + assertTrue(evaluationData.allConditionsAreMatched(conditions, context)); // not match browser.clear(); browser.put("name", "Chrome"); context.put("browser", browser); - assertFalse(datafileReader.allConditionsAreMatched(conditions, context)); + assertFalse(evaluationData.allConditionsAreMatched(conditions, context)); } @Test @@ -208,11 +251,11 @@ public void testOperatorEndsWith() { conditions.add(condition); // match - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("name", "Hello World"))); - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("name", "Hi World"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("name", "Hello World"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("name", "Hi World"))); // not match - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("name", "Hi Universe"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("name", "Hi Universe"))); } @Test @@ -228,12 +271,12 @@ public void testOperatorIncludes() { List permissions = new ArrayList<>(); permissions.add("read"); permissions.add("write"); - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("permissions", permissions))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("permissions", permissions))); // not match permissions.clear(); permissions.add("read"); - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("permissions", permissions))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("permissions", permissions))); } @Test @@ -249,11 +292,11 @@ public void testOperatorNotIncludes() { List permissions = new ArrayList<>(); permissions.add("read"); permissions.add("admin"); - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("permissions", permissions))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("permissions", permissions))); // not match permissions.add("write"); - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("permissions", permissions))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("permissions", permissions))); } @Test @@ -266,11 +309,11 @@ public void testOperatorContains() { conditions.add(condition); // match - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("name", "Hello World"))); - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("name", "Yo! Hello!"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("name", "Hello World"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("name", "Yo! Hello!"))); // not match - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("name", "Hi World"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("name", "Hi World"))); } @Test @@ -283,11 +326,11 @@ public void testOperatorNotContains() { conditions.add(condition); // match - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("name", "Hi World"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("name", "Hi World"))); // not match - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("name", "Hello World"))); - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("name", "Yo! Hello!"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("name", "Hello World"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("name", "Yo! Hello!"))); } @Test @@ -300,14 +343,14 @@ public void testOperatorMatches() { conditions.add(condition); // match - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("name", "Hello"))); - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("name", "Helloooooo"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("name", "Hello"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("name", "Helloooooo"))); // not match - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("name", "Hello World"))); - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("name", "Hell123"))); - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("name", "123"))); - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("name", 123))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("name", "Hello World"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("name", "Hell123"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("name", "123"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("name", 123))); } @Test @@ -321,14 +364,14 @@ public void testOperatorMatchesWithRegexFlags() { conditions.add(condition); // match - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("name", "Hello"))); - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("name", "Helloooooo"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("name", "Hello"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("name", "Helloooooo"))); // not match - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("name", "Hello World"))); - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("name", "Hell123"))); - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("name", "123"))); - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("name", 123))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("name", "Hello World"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("name", "Hell123"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("name", "123"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("name", 123))); } @Test @@ -341,12 +384,12 @@ public void testOperatorNotMatches() { conditions.add(condition); // match - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("name", "Hi World"))); - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("name", "123"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("name", "Hi World"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("name", "123"))); // not match - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("name", "Hello"))); - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("name", "Hellooooooo"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("name", "Hello"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("name", "Hellooooooo"))); } @Test @@ -360,12 +403,12 @@ public void testOperatorNotMatchesWithRegexFlags() { conditions.add(condition); // match - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("name", "Hi World"))); - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("name", "123"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("name", "Hi World"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("name", "123"))); // not match - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("name", "Hello"))); - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("name", "Hellooooooo"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("name", "Hello"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("name", "Hellooooooo"))); } @Test @@ -381,12 +424,12 @@ public void testOperatorIn() { conditions.add(condition); // match - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("browser_type", "chrome"))); - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("browser_type", "firefox"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("browser_type", "chrome"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("browser_type", "firefox"))); // not match - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("browser_type", "edge"))); - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("browser_type", "safari"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("browser_type", "edge"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("browser_type", "safari"))); } @Test @@ -402,12 +445,12 @@ public void testOperatorNotIn() { conditions.add(condition); // match - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("browser_type", "edge"))); - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("browser_type", "safari"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("browser_type", "edge"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("browser_type", "safari"))); // not match - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("browser_type", "chrome"))); - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("browser_type", "firefox"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("browser_type", "chrome"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("browser_type", "firefox"))); } @Test @@ -420,10 +463,10 @@ public void testOperatorGreaterThan() { conditions.add(condition); // match - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("age", 19))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("age", 19))); // not match - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("age", 17))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("age", 17))); } @Test @@ -436,12 +479,12 @@ public void testOperatorGreaterThanOrEquals() { conditions.add(condition); // match - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("age", 18))); - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("age", 19))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("age", 18))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("age", 19))); // not match - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("age", 17))); - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("age", 16))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("age", 17))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("age", 16))); } @Test @@ -454,10 +497,10 @@ public void testOperatorLessThan() { conditions.add(condition); // match - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("age", 17))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("age", 17))); // not match - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("age", 19))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("age", 19))); } @Test @@ -470,12 +513,12 @@ public void testOperatorLessThanOrEquals() { conditions.add(condition); // match - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("age", 17))); - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("age", 18))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("age", 17))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("age", 18))); // not match - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("age", 19))); - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("age", 20))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("age", 19))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("age", 20))); } @Test @@ -488,10 +531,10 @@ public void testOperatorSemverEquals() { conditions.add(condition); // match - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("version", "1.0.0"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("version", "1.0.0"))); // not match - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("version", "2.0.0"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("version", "2.0.0"))); } @Test @@ -504,10 +547,10 @@ public void testOperatorSemverNotEquals() { conditions.add(condition); // match - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("version", "2.0.0"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("version", "2.0.0"))); // not match - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("version", "1.0.0"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("version", "1.0.0"))); } @Test @@ -520,10 +563,10 @@ public void testOperatorSemverGreaterThan() { conditions.add(condition); // match - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("version", "2.0.0"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("version", "2.0.0"))); // not match - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("version", "0.9.0"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("version", "0.9.0"))); } @Test @@ -536,11 +579,11 @@ public void testOperatorSemverGreaterThanOrEquals() { conditions.add(condition); // match - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("version", "1.0.0"))); - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("version", "2.0.0"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("version", "1.0.0"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("version", "2.0.0"))); // not match - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("version", "0.9.0"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("version", "0.9.0"))); } @Test @@ -553,10 +596,10 @@ public void testOperatorSemverLessThan() { conditions.add(condition); // match - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("version", "0.9.0"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("version", "0.9.0"))); // not match - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("version", "1.1.0"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("version", "1.1.0"))); } @Test @@ -569,10 +612,25 @@ public void testOperatorSemverLessThanOrEquals() { conditions.add(condition); // match - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("version", "1.0.0"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("version", "1.0.0"))); // not match - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("version", "1.1.0"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("version", "1.1.0"))); + } + + @Test + public void testSemverPrereleaseAndBuildMetadata() { + Condition prerelease = new Condition(); + prerelease.setAttribute("version"); + prerelease.setOperator(Operator.SEMVER_LESS_THAN); + prerelease.setValue("1.2.3"); + assertTrue(evaluationData.allConditionsAreMatched(prerelease, Map.of("version", "1.2.3-beta.1"))); + + Condition build = new Condition(); + build.setAttribute("version"); + build.setOperator(Operator.SEMVER_EQUALS); + build.setValue("1.2.3+build.9"); + assertTrue(evaluationData.allConditionsAreMatched(build, Map.of("version", "1.2.3+build.5"))); } @Test @@ -585,10 +643,17 @@ public void testOperatorBefore() { conditions.add(condition); // match - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("date", "2023-05-12T00:00:00Z"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("date", "2023-05-12T00:00:00Z"))); // not match - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("date", "2023-05-14T00:00:00Z"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("date", "2023-05-14T00:00:00Z"))); + + // equivalent offset instant is not before + condition.setValue("2023-05-13T17:23:59+01:00"); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("date", "2023-05-13T16:23:59Z"))); + + // strings without an explicit timezone are not portable + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("date", "2023-05-12T00:00:00"))); } @Test @@ -601,10 +666,10 @@ public void testOperatorAfter() { conditions.add(condition); // match - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("date", "2023-05-14T00:00:00Z"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("date", "2023-05-14T00:00:00Z"))); // not match - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("date", "2023-05-12T00:00:00Z"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("date", "2023-05-12T00:00:00Z"))); } @Test @@ -617,7 +682,7 @@ public void testSimpleConditionExactSingleCondition() { conditions.add(condition); // match - assertTrue(datafileReader.allConditionsAreMatched(conditions.get(0), Map.of("browser_type", "chrome"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions.get(0), Map.of("browser_type", "chrome"))); } @Test @@ -630,7 +695,7 @@ public void testSimpleConditionExactCondition() { conditions.add(condition); // match - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("browser_type", "chrome"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("browser_type", "chrome"))); } @Test @@ -638,8 +703,8 @@ public void testSimpleConditionEmptyConditions() { List conditions = new ArrayList<>(); // Empty conditions should match everything - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("browser_type", "chrome"))); - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("browser_type", "firefox"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("browser_type", "chrome"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("browser_type", "firefox"))); } @Test @@ -655,7 +720,7 @@ public void testSimpleConditionExtraConditions() { Map context = new HashMap<>(); context.put("browser_type", "chrome"); context.put("browser_version", "1.0"); - assertTrue(datafileReader.allConditionsAreMatched(conditions, context)); + assertTrue(evaluationData.allConditionsAreMatched(conditions, context)); } @Test @@ -679,7 +744,7 @@ public void testSimpleConditionMultipleConditions() { context.put("browser_type", "chrome"); context.put("browser_version", "1.0"); context.put("foo", "bar"); - assertTrue(datafileReader.allConditionsAreMatched(conditions, context)); + assertTrue(evaluationData.allConditionsAreMatched(conditions, context)); } @Test @@ -699,10 +764,10 @@ public void testAndConditionOneCondition() { conditions.add(andCondition); // match - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("browser_type", "chrome"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("browser_type", "chrome"))); // not match - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("browser_type", "firefox"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("browser_type", "firefox"))); } @Test @@ -731,12 +796,12 @@ public void testAndConditionMultipleConditions() { Map context = new HashMap<>(); context.put("browser_type", "chrome"); context.put("browser_version", "1.0"); - assertTrue(datafileReader.allConditionsAreMatched(conditions, context)); + assertTrue(evaluationData.allConditionsAreMatched(conditions, context)); // not match context.clear(); context.put("browser_type", "chrome"); - assertFalse(datafileReader.allConditionsAreMatched(conditions, context)); + assertFalse(evaluationData.allConditionsAreMatched(conditions, context)); } @Test @@ -756,7 +821,7 @@ public void testOrConditionOneCondition() { conditions.add(orCondition); // match - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("browser_type", "chrome"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("browser_type", "chrome"))); } @Test @@ -782,10 +847,10 @@ public void testOrConditionMultipleConditions() { conditions.add(orCondition); // match - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("browser_version", "1.0"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("browser_version", "1.0"))); // not match - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("browser_type", "firefox"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("browser_type", "firefox"))); } @Test @@ -805,10 +870,10 @@ public void testNotConditionOneCondition() { conditions.add(notCondition); // match (not chrome) - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("browser_type", "firefox"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("browser_type", "firefox"))); // not match (is chrome) - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("browser_type", "chrome"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("browser_type", "chrome"))); } @Test @@ -837,19 +902,19 @@ public void testNotConditionMultipleConditions() { Map context = new HashMap<>(); context.put("browser_type", "firefox"); context.put("browser_version", "2.0"); - assertTrue(datafileReader.allConditionsAreMatched(conditions, context)); + assertTrue(evaluationData.allConditionsAreMatched(conditions, context)); // not match (both conditions match) context.clear(); context.put("browser_type", "chrome"); context.put("browser_version", "1.0"); - assertFalse(datafileReader.allConditionsAreMatched(conditions, context)); + assertFalse(evaluationData.allConditionsAreMatched(conditions, context)); // match (only one condition matches) context.clear(); context.put("browser_type", "chrome"); context.put("browser_version", "2.0"); - assertTrue(datafileReader.allConditionsAreMatched(conditions, context)); + assertTrue(evaluationData.allConditionsAreMatched(conditions, context)); } @Test @@ -870,12 +935,12 @@ public void testNotConditionNestedOrMeansNoneMatchAndEmptyNotIsFalse() { Map notCondition = new HashMap<>(); notCondition.put("not", List.of(orCondition)); - assertFalse(datafileReader.allConditionsAreMatched(List.of(notCondition), Map.of("browser_type", "chrome"))); - assertTrue(datafileReader.allConditionsAreMatched(List.of(notCondition), Map.of("browser_type", "edge"))); + assertFalse(evaluationData.allConditionsAreMatched(List.of(notCondition), Map.of("browser_type", "chrome"))); + assertTrue(evaluationData.allConditionsAreMatched(List.of(notCondition), Map.of("browser_type", "edge"))); Map emptyNotCondition = new HashMap<>(); emptyNotCondition.put("not", List.of()); - assertFalse(datafileReader.allConditionsAreMatched(List.of(emptyNotCondition), Map.of())); + assertFalse(evaluationData.allConditionsAreMatched(List.of(emptyNotCondition), Map.of())); } @Test @@ -912,12 +977,12 @@ public void testNestedConditionsOrInsideAnd() { conditions.add(andCondition); // match - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("browser_type", "chrome", "browser_version", "1.0"))); - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("browser_type", "chrome", "browser_version", "2.0"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("browser_type", "chrome", "browser_version", "1.0"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("browser_type", "chrome", "browser_version", "2.0"))); // not match - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("browser_type", "chrome", "browser_version", "3.0"))); - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("browser_version", "2.0"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("browser_type", "chrome", "browser_version", "3.0"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("browser_version", "2.0"))); } @Test @@ -960,12 +1025,12 @@ public void testNestedConditionsPlainConditionsFollowedByOrInsideAnd() { conditions.add(andCondition); // match - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("country", "nl", "browser_type", "chrome", "browser_version", "1.0"))); - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("country", "nl", "browser_type", "chrome", "browser_version", "2.0"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("country", "nl", "browser_type", "chrome", "browser_version", "1.0"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("country", "nl", "browser_type", "chrome", "browser_version", "2.0"))); // not match - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("browser_type", "chrome", "browser_version", "3.0"))); - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("country", "us", "browser_version", "2.0"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("browser_type", "chrome", "browser_version", "3.0"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("country", "us", "browser_version", "2.0"))); } @Test @@ -1002,12 +1067,12 @@ public void testNestedConditionsAndInsideOr() { conditions.add(orCondition); // match - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("browser_type", "chrome"))); - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("browser_version", "1.0", "country", "nl"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("browser_type", "chrome"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("browser_version", "1.0", "country", "nl"))); // not match - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("browser_type", "firefox", "browser_version", "1.0"))); - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("browser_version", "1.0", "country", "us"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("browser_type", "firefox", "browser_version", "1.0"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("browser_version", "1.0", "country", "us"))); } @Test @@ -1050,11 +1115,11 @@ public void testNestedConditionsPlainConditionsFollowedByAndInsideOr() { conditions.add(orCondition); // match - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("country", "nl", "browser_type", "chrome"))); - assertTrue(datafileReader.allConditionsAreMatched(conditions, Map.of("country", "nl", "browser_version", "1.0", "device_type", "mobile"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("country", "nl", "browser_type", "chrome"))); + assertTrue(evaluationData.allConditionsAreMatched(conditions, Map.of("country", "nl", "browser_version", "1.0", "device_type", "mobile"))); // not match - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("browser_type", "chrome"))); - assertFalse(datafileReader.allConditionsAreMatched(conditions, Map.of("country", "nl", "browser_version", "1.0"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("browser_type", "chrome"))); + assertFalse(evaluationData.allConditionsAreMatched(conditions, Map.of("country", "nl", "browser_version", "1.0"))); } } diff --git a/featurevisor-sdk/src/test/java/com/featurevisor/sdk/LoggerTest.java b/featurevisor-sdk/src/test/java/com/featurevisor/sdk/DiagnosticReporterTest.java similarity index 59% rename from featurevisor-sdk/src/test/java/com/featurevisor/sdk/LoggerTest.java rename to featurevisor-sdk/src/test/java/com/featurevisor/sdk/DiagnosticReporterTest.java index 13e9867..2825c7a 100644 --- a/featurevisor-sdk/src/test/java/com/featurevisor/sdk/LoggerTest.java +++ b/featurevisor-sdk/src/test/java/com/featurevisor/sdk/DiagnosticReporterTest.java @@ -12,7 +12,7 @@ import java.util.List; import java.util.ArrayList; -public class LoggerTest { +public class DiagnosticReporterTest { private ByteArrayOutputStream outputStream; private PrintStream originalOut; @@ -34,35 +34,35 @@ public void tearDown() { } @Test - public void testCreateLoggerWithDefaultOptions() { - Logger logger = Logger.createLogger(); - assertNotNull(logger); - assertTrue(logger instanceof Logger); + public void testCreateDiagnosticReporterWithDefaultOptions() { + DiagnosticReporter diagnostics = DiagnosticReporter.createDiagnosticReporter(); + assertNotNull(diagnostics); + assertTrue(diagnostics instanceof DiagnosticReporter); } @Test - public void testCreateLoggerWithCustomLevel() { - Logger logger = Logger.createLogger(new Logger.CreateLoggerOptions().level(FeaturevisorLogLevel.DEBUG)); - assertNotNull(logger); - assertTrue(logger instanceof Logger); + public void testCreateDiagnosticReporterWithCustomLevel() { + DiagnosticReporter diagnostics = DiagnosticReporter.createDiagnosticReporter(new DiagnosticReporter.DiagnosticReporterOptions().level(FeaturevisorLogLevel.DEBUG)); + assertNotNull(diagnostics); + assertTrue(diagnostics instanceof DiagnosticReporter); } @Test - public void testCreateLoggerWithCustomHandler() { + public void testCreateDiagnosticReporterWithCustomHandler() { final boolean[] handlerCalled = {false}; final String[] capturedLevel = {null}; final String[] capturedMessage = {null}; final Object[] capturedDetails = {null}; - Logger.LogHandler customHandler = (level, message, details) -> { + DiagnosticReporter.DiagnosticOutputHandler customHandler = (level, message, details) -> { handlerCalled[0] = true; capturedLevel[0] = level.name().toLowerCase(); capturedMessage[0] = message; capturedDetails[0] = details; }; - Logger logger = Logger.createLogger(new Logger.CreateLoggerOptions().handler(customHandler)); - logger.info("test message"); + DiagnosticReporter diagnostics = DiagnosticReporter.createDiagnosticReporter(new DiagnosticReporter.DiagnosticReporterOptions().handler(customHandler)); + diagnostics.info("test message"); assertTrue(handlerCalled[0]); assertEquals("info", capturedLevel[0]); @@ -71,9 +71,9 @@ public void testCreateLoggerWithCustomHandler() { } @Test - public void testLoggerConstructorWithDefaultLevel() { - Logger logger = new Logger(new Logger.CreateLoggerOptions()); - logger.debug("debug message"); + public void testDiagnosticReporterConstructorWithDefaultLevel() { + DiagnosticReporter diagnostics = new DiagnosticReporter(new DiagnosticReporter.DiagnosticReporterOptions()); + diagnostics.debug("debug message"); // Debug should not be logged with default level (info) String output = outputStream.toString(); @@ -81,9 +81,9 @@ public void testLoggerConstructorWithDefaultLevel() { } @Test - public void testLoggerConstructorWithProvidedLevel() { - Logger logger = new Logger(new Logger.CreateLoggerOptions().level(FeaturevisorLogLevel.DEBUG)); - logger.debug("debug message"); + public void testDiagnosticReporterConstructorWithProvidedLevel() { + DiagnosticReporter diagnostics = new DiagnosticReporter(new DiagnosticReporter.DiagnosticReporterOptions().level(FeaturevisorLogLevel.DEBUG)); + diagnostics.debug("debug message"); // Debug should be logged with debug level String output = outputStream.toString(); @@ -92,9 +92,9 @@ public void testLoggerConstructorWithProvidedLevel() { } @Test - public void testLoggerConstructorWithDefaultHandler() { - Logger logger = new Logger(new Logger.CreateLoggerOptions()); - logger.info("test message"); + public void testDiagnosticReporterConstructorWithDefaultHandler() { + DiagnosticReporter diagnostics = new DiagnosticReporter(new DiagnosticReporter.DiagnosticReporterOptions()); + diagnostics.info("test message"); String output = outputStream.toString(); assertTrue(output.contains("[Featurevisor]")); @@ -102,21 +102,21 @@ public void testLoggerConstructorWithDefaultHandler() { } @Test - public void testLoggerConstructorWithProvidedHandler() { + public void testDiagnosticReporterConstructorWithProvidedHandler() { final boolean[] handlerCalled = {false}; final String[] capturedLevel = {null}; final String[] capturedMessage = {null}; final Object[] capturedDetails = {null}; - Logger.LogHandler customHandler = (level, message, details) -> { + DiagnosticReporter.DiagnosticOutputHandler customHandler = (level, message, details) -> { handlerCalled[0] = true; capturedLevel[0] = level.name().toLowerCase(); capturedMessage[0] = message; capturedDetails[0] = details; }; - Logger logger = new Logger(new Logger.CreateLoggerOptions().handler(customHandler)); - logger.info("test message"); + DiagnosticReporter diagnostics = new DiagnosticReporter(new DiagnosticReporter.DiagnosticReporterOptions().handler(customHandler)); + diagnostics.info("test message"); assertTrue(handlerCalled[0]); assertEquals("info", capturedLevel[0]); @@ -126,10 +126,10 @@ public void testLoggerConstructorWithProvidedHandler() { @Test public void testSetLevel() { - Logger logger = new Logger(new Logger.CreateLoggerOptions().level(FeaturevisorLogLevel.INFO)); + DiagnosticReporter diagnostics = new DiagnosticReporter(new DiagnosticReporter.DiagnosticReporterOptions().level(FeaturevisorLogLevel.INFO)); // Debug should not be logged initially - logger.debug("debug message"); + diagnostics.debug("debug message"); String output = outputStream.toString(); assertFalse(output.contains("debug message")); @@ -137,8 +137,8 @@ public void testSetLevel() { outputStream.reset(); // Set to debug level - logger.setLevel(FeaturevisorLogLevel.DEBUG); - logger.debug("debug message"); + diagnostics.setLevel(FeaturevisorLogLevel.DEBUG); + diagnostics.debug("debug message"); output = outputStream.toString(); assertTrue(output.contains("debug message")); } @@ -148,8 +148,8 @@ public void testLogLevelFilteringErrorMessages() { FeaturevisorLogLevel[] levels = {FeaturevisorLogLevel.DEBUG, FeaturevisorLogLevel.INFO, FeaturevisorLogLevel.WARN, FeaturevisorLogLevel.ERROR}; for (FeaturevisorLogLevel level : levels) { - Logger logger = new Logger(new Logger.CreateLoggerOptions().level(level)); - logger.error("error message"); + DiagnosticReporter diagnostics = new DiagnosticReporter(new DiagnosticReporter.DiagnosticReporterOptions().level(level)); + diagnostics.error("error message"); String output = outputStream.toString(); assertTrue(output.contains("error message")); @@ -161,104 +161,104 @@ public void testLogLevelFilteringErrorMessages() { @Test public void testLogLevelFilteringWarnMessages() { - Logger logger = new Logger(new Logger.CreateLoggerOptions().level(FeaturevisorLogLevel.WARN)); + DiagnosticReporter diagnostics = new DiagnosticReporter(new DiagnosticReporter.DiagnosticReporterOptions().level(FeaturevisorLogLevel.WARN)); - logger.warn("warn message"); + diagnostics.warn("warn message"); String output = outputStream.toString(); assertTrue(output.contains("warn message")); outputStream.reset(); - logger.error("error message"); + diagnostics.error("error message"); output = outputStream.toString(); assertTrue(output.contains("error message")); } @Test public void testLogLevelFilteringInfoMessages() { - Logger logger = new Logger(new Logger.CreateLoggerOptions().level(FeaturevisorLogLevel.WARN)); + DiagnosticReporter diagnostics = new DiagnosticReporter(new DiagnosticReporter.DiagnosticReporterOptions().level(FeaturevisorLogLevel.WARN)); - logger.info("info message"); + diagnostics.info("info message"); String output = outputStream.toString(); assertFalse(output.contains("info message")); } @Test public void testLogLevelFilteringDebugMessages() { - Logger logger = new Logger(new Logger.CreateLoggerOptions().level(FeaturevisorLogLevel.INFO)); + DiagnosticReporter diagnostics = new DiagnosticReporter(new DiagnosticReporter.DiagnosticReporterOptions().level(FeaturevisorLogLevel.INFO)); - logger.debug("debug message"); + diagnostics.debug("debug message"); String output = outputStream.toString(); assertFalse(output.contains("debug message")); } @Test public void testLogLevelFilteringDebugLevel() { - Logger logger = new Logger(new Logger.CreateLoggerOptions().level(FeaturevisorLogLevel.DEBUG)); + DiagnosticReporter diagnostics = new DiagnosticReporter(new DiagnosticReporter.DiagnosticReporterOptions().level(FeaturevisorLogLevel.DEBUG)); - logger.debug("debug message"); + diagnostics.debug("debug message"); String output = outputStream.toString(); assertTrue(output.contains("debug message")); outputStream.reset(); - logger.info("info message"); + diagnostics.info("info message"); output = outputStream.toString(); assertTrue(output.contains("info message")); outputStream.reset(); - logger.warn("warn message"); + diagnostics.warn("warn message"); output = outputStream.toString(); assertTrue(output.contains("warn message")); outputStream.reset(); - logger.error("error message"); + diagnostics.error("error message"); output = outputStream.toString(); assertTrue(output.contains("error message")); } @Test public void testConvenienceMethods() { - Logger logger = new Logger(new Logger.CreateLoggerOptions().level(FeaturevisorLogLevel.DEBUG)); + DiagnosticReporter diagnostics = new DiagnosticReporter(new DiagnosticReporter.DiagnosticReporterOptions().level(FeaturevisorLogLevel.DEBUG)); // Test debug method - logger.debug("debug message"); + diagnostics.debug("debug message"); String output = outputStream.toString(); assertTrue(output.contains("debug message")); outputStream.reset(); // Test info method - logger.info("info message"); + diagnostics.info("info message"); output = outputStream.toString(); assertTrue(output.contains("info message")); outputStream.reset(); // Test warn method - logger.warn("warn message"); + diagnostics.warn("warn message"); output = outputStream.toString(); assertTrue(output.contains("warn message")); outputStream.reset(); // Test error method - logger.error("error message"); + diagnostics.error("error message"); output = outputStream.toString(); assertTrue(output.contains("error message")); } @Test public void testHandleDetailsParameter() { - Logger logger = new Logger(new Logger.CreateLoggerOptions().level(FeaturevisorLogLevel.INFO)); + DiagnosticReporter diagnostics = new DiagnosticReporter(new DiagnosticReporter.DiagnosticReporterOptions().level(FeaturevisorLogLevel.INFO)); Map details = new HashMap<>(); details.put("key", "value"); details.put("number", 42); - logger.info("message with details", details); + diagnostics.info("message with details", details); String output = outputStream.toString(); assertTrue(output.contains("message with details")); // Note: The exact format of details in output may vary based on implementation @@ -271,21 +271,21 @@ public void testLogMethodWithCustomHandler() { final String[] capturedMessage = {null}; final Object[] capturedDetails = {null}; - Logger.LogHandler customHandler = (level, message, details) -> { + DiagnosticReporter.DiagnosticOutputHandler customHandler = (level, message, details) -> { handlerCalled[0] = true; capturedLevel[0] = level.name().toLowerCase(); capturedMessage[0] = message; capturedDetails[0] = details; }; - Logger logger = new Logger(new Logger.CreateLoggerOptions() + DiagnosticReporter diagnostics = new DiagnosticReporter(new DiagnosticReporter.DiagnosticReporterOptions() .handler(customHandler) .level(FeaturevisorLogLevel.DEBUG)); Map details = new HashMap<>(); details.put("test", true); - logger.log(FeaturevisorLogLevel.INFO, "test message", details); + diagnostics.log(FeaturevisorLogLevel.INFO, "test message", details); assertTrue(handlerCalled[0]); assertEquals("info", capturedLevel[0]); @@ -294,29 +294,29 @@ public void testLogMethodWithCustomHandler() { } @Test - public void testLogMethodWithLevelFiltering() { + public void testCustomEvaluatorSinkReceivesAllLevels() { final boolean[] handlerCalled = {false}; - Logger.LogHandler customHandler = (level, message, details) -> { + DiagnosticReporter.DiagnosticOutputHandler customHandler = (level, message, details) -> { handlerCalled[0] = true; }; - Logger logger = new Logger(new Logger.CreateLoggerOptions() + DiagnosticReporter diagnostics = new DiagnosticReporter(new DiagnosticReporter.DiagnosticReporterOptions() .handler(customHandler) .level(FeaturevisorLogLevel.WARN)); - logger.log(FeaturevisorLogLevel.DEBUG, "debug message", null); + diagnostics.log(FeaturevisorLogLevel.DEBUG, "debug message", null); - assertFalse(handlerCalled[0]); + assertTrue(handlerCalled[0]); } @Test - public void testDefaultLogHandler() { - // Test that default handler works through the logger - Logger logger = new Logger(new Logger.CreateLoggerOptions().level(FeaturevisorLogLevel.DEBUG)); + public void testDefaultDiagnosticOutputHandler() { + // Test that default handler works through the diagnostics + DiagnosticReporter diagnostics = new DiagnosticReporter(new DiagnosticReporter.DiagnosticReporterOptions().level(FeaturevisorLogLevel.DEBUG)); // Test debug level - logger.debug("debug message"); + diagnostics.debug("debug message"); String output = outputStream.toString(); assertTrue(output.contains("[Featurevisor]")); assertTrue(output.contains("debug message")); @@ -324,7 +324,7 @@ public void testDefaultLogHandler() { outputStream.reset(); // Test info level - logger.info("info message"); + diagnostics.info("info message"); output = outputStream.toString(); assertTrue(output.contains("[Featurevisor]")); assertTrue(output.contains("info message")); @@ -332,7 +332,7 @@ public void testDefaultLogHandler() { outputStream.reset(); // Test warn level - logger.warn("warn message"); + diagnostics.warn("warn message"); output = outputStream.toString(); assertTrue(output.contains("[Featurevisor]")); assertTrue(output.contains("warn message")); @@ -340,28 +340,28 @@ public void testDefaultLogHandler() { outputStream.reset(); // Test error level - logger.error("error message"); + diagnostics.error("error message"); output = outputStream.toString(); assertTrue(output.contains("[Featurevisor]")); assertTrue(output.contains("error message")); } @Test - public void testDefaultLogHandlerWithUndefinedDetails() { - Logger logger = new Logger(new Logger.CreateLoggerOptions()); - logger.info("message without details"); + public void testDefaultDiagnosticOutputHandlerWithUndefinedDetails() { + DiagnosticReporter diagnostics = new DiagnosticReporter(new DiagnosticReporter.DiagnosticReporterOptions()); + diagnostics.info("message without details"); String output = outputStream.toString(); assertTrue(output.contains("[Featurevisor]")); assertTrue(output.contains("message without details")); } @Test - public void testDefaultLogHandlerWithProvidedDetails() { - Logger logger = new Logger(new Logger.CreateLoggerOptions()); + public void testDefaultDiagnosticOutputHandlerWithProvidedDetails() { + DiagnosticReporter diagnostics = new DiagnosticReporter(new DiagnosticReporter.DiagnosticReporterOptions()); Map details = new HashMap<>(); details.put("key", "value"); - logger.info("message with details", details); + diagnostics.info("message with details", details); String output = outputStream.toString(); assertTrue(output.contains("[Featurevisor]")); assertTrue(output.contains("message with details")); @@ -378,15 +378,15 @@ public void testLogLevelEnumValues() { } @Test - public void testCreateLoggerOptionsBuilder() { - Logger.LogHandler handler = (level, message, details) -> {}; - Logger.CreateLoggerOptions options = new Logger.CreateLoggerOptions() + public void testDiagnosticReporterOptionsBuilder() { + DiagnosticReporter.DiagnosticOutputHandler handler = (level, message, details) -> {}; + DiagnosticReporter.DiagnosticReporterOptions options = new DiagnosticReporter.DiagnosticReporterOptions() .level(FeaturevisorLogLevel.DEBUG) .handler(handler); // Test that the builder pattern works correctly - Logger logger = new Logger(options); - assertNotNull(logger); + DiagnosticReporter diagnostics = new DiagnosticReporter(options); + assertNotNull(diagnostics); } @Test @@ -399,22 +399,22 @@ public void testLogLevelEnumOrdinal() { } @Test - public void testLoggerWithNullMessage() { - Logger logger = new Logger(new Logger.CreateLoggerOptions().level(FeaturevisorLogLevel.DEBUG)); + public void testDiagnosticReporterWithNullMessage() { + DiagnosticReporter diagnostics = new DiagnosticReporter(new DiagnosticReporter.DiagnosticReporterOptions().level(FeaturevisorLogLevel.DEBUG)); // Should not throw exception with null message assertDoesNotThrow(() -> { - logger.info(null); + diagnostics.info(null); }); } @Test - public void testLoggerWithEmptyMessage() { - Logger logger = new Logger(new Logger.CreateLoggerOptions().level(FeaturevisorLogLevel.DEBUG)); + public void testDiagnosticReporterWithEmptyMessage() { + DiagnosticReporter diagnostics = new DiagnosticReporter(new DiagnosticReporter.DiagnosticReporterOptions().level(FeaturevisorLogLevel.DEBUG)); // Should handle empty message assertDoesNotThrow(() -> { - logger.info(""); + diagnostics.info(""); }); String output = outputStream.toString(); @@ -422,12 +422,12 @@ public void testLoggerWithEmptyMessage() { } @Test - public void testLoggerWithNullDetails() { - Logger logger = new Logger(new Logger.CreateLoggerOptions().level(FeaturevisorLogLevel.DEBUG)); + public void testDiagnosticReporterWithNullDetails() { + DiagnosticReporter diagnostics = new DiagnosticReporter(new DiagnosticReporter.DiagnosticReporterOptions().level(FeaturevisorLogLevel.DEBUG)); // Should handle null details assertDoesNotThrow(() -> { - logger.info("test message", null); + diagnostics.info("test message", null); }); String output = outputStream.toString(); @@ -435,8 +435,8 @@ public void testLoggerWithNullDetails() { } @Test - public void testLoggerWithComplexDetails() { - Logger logger = new Logger(new Logger.CreateLoggerOptions().level(FeaturevisorLogLevel.DEBUG)); + public void testDiagnosticReporterWithComplexDetails() { + DiagnosticReporter diagnostics = new DiagnosticReporter(new DiagnosticReporter.DiagnosticReporterOptions().level(FeaturevisorLogLevel.DEBUG)); Map complexDetails = new HashMap<>(); complexDetails.put("string", "value"); @@ -454,7 +454,7 @@ public void testLoggerWithComplexDetails() { complexDetails.put("nested", nested); assertDoesNotThrow(() -> { - logger.info("complex message", complexDetails); + diagnostics.info("complex message", complexDetails); }); String output = outputStream.toString(); diff --git a/featurevisor-sdk/src/test/java/com/featurevisor/sdk/EmitterTest.java b/featurevisor-sdk/src/test/java/com/featurevisor/sdk/EmitterTest.java index dfb8eb4..83d0553 100644 --- a/featurevisor-sdk/src/test/java/com/featurevisor/sdk/EmitterTest.java +++ b/featurevisor-sdk/src/test/java/com/featurevisor/sdk/EmitterTest.java @@ -10,7 +10,7 @@ public class EmitterTest { private Emitter emitter; - private List handledDetails; + private List handledDetails; @BeforeEach public void setUp() { @@ -18,46 +18,46 @@ public void setUp() { handledDetails = new ArrayList<>(); } - private void handleDetails(Emitter.EventDetails details) { + private void handleDetails(FeaturevisorEventDetails details) { handledDetails.add(details); } @Test public void testAddListenerForEvent() { // Add a listener for datafile_set event - Emitter.UnsubscribeFunction unsubscribe = emitter.on(Emitter.EventName.DATAFILE_SET, this::handleDetails); + FeaturevisorUnsubscribe unsubscribe = emitter.on(FeaturevisorEventName.DATAFILE_SET, this::handleDetails); // Verify the listener was added - assertTrue(emitter.getListeners().containsKey(Emitter.EventName.DATAFILE_SET)); - assertTrue(emitter.getListeners().get(Emitter.EventName.DATAFILE_SET).size() > 0); + assertTrue(emitter.getListeners().containsKey(FeaturevisorEventName.DATAFILE_SET)); + assertTrue(emitter.getListeners().get(FeaturevisorEventName.DATAFILE_SET).size() > 0); // Verify other events don't have listeners - assertFalse(emitter.getListeners().containsKey(Emitter.EventName.CONTEXT_SET)); - assertFalse(emitter.getListeners().containsKey(Emitter.EventName.STICKY_SET)); + assertFalse(emitter.getListeners().containsKey(FeaturevisorEventName.CONTEXT_SET)); + assertFalse(emitter.getListeners().containsKey(FeaturevisorEventName.STICKY_SET)); // Verify there's exactly one listener - assertEquals(1, emitter.getListeners().get(Emitter.EventName.DATAFILE_SET).size()); + assertEquals(1, emitter.getListeners().get(FeaturevisorEventName.DATAFILE_SET).size()); // Trigger the subscribed event - Emitter.EventDetails details1 = new Emitter.EventDetails(); + FeaturevisorEventDetails details1 = new FeaturevisorEventDetails(); details1.put("key", "value"); - emitter.trigger(Emitter.EventName.DATAFILE_SET, details1); + emitter.trigger(FeaturevisorEventName.DATAFILE_SET, details1); // Verify the callback was called assertEquals(1, handledDetails.size()); assertEquals("value", handledDetails.get(0).get("key")); // Trigger an unsubscribed event - Emitter.EventDetails details2 = new Emitter.EventDetails(); + FeaturevisorEventDetails details2 = new FeaturevisorEventDetails(); details2.put("key", "value2"); - emitter.trigger(Emitter.EventName.STICKY_SET, details2); + emitter.trigger(FeaturevisorEventName.STICKY_SET, details2); // Verify the callback was not called for the unsubscribed event assertEquals(1, handledDetails.size()); // Unsubscribe unsubscribe.unsubscribe(); - assertEquals(0, emitter.getListeners().get(Emitter.EventName.DATAFILE_SET).size()); + assertEquals(0, emitter.getListeners().get(FeaturevisorEventName.DATAFILE_SET).size()); // Clear all emitter.clearAll(); @@ -66,19 +66,19 @@ public void testAddListenerForEvent() { @Test public void testMultipleListeners() { - List secondHandler = new ArrayList<>(); + List secondHandler = new ArrayList<>(); // Add two listeners for the same event - Emitter.UnsubscribeFunction unsubscribe1 = emitter.on(Emitter.EventName.CONTEXT_SET, this::handleDetails); - Emitter.UnsubscribeFunction unsubscribe2 = emitter.on(Emitter.EventName.CONTEXT_SET, secondHandler::add); + FeaturevisorUnsubscribe unsubscribe1 = emitter.on(FeaturevisorEventName.CONTEXT_SET, this::handleDetails); + FeaturevisorUnsubscribe unsubscribe2 = emitter.on(FeaturevisorEventName.CONTEXT_SET, secondHandler::add); // Verify both listeners were added - assertEquals(2, emitter.getListeners().get(Emitter.EventName.CONTEXT_SET).size()); + assertEquals(2, emitter.getListeners().get(FeaturevisorEventName.CONTEXT_SET).size()); // Trigger the event - Emitter.EventDetails details = new Emitter.EventDetails(); + FeaturevisorEventDetails details = new FeaturevisorEventDetails(); details.put("test", "multiple"); - emitter.trigger(Emitter.EventName.CONTEXT_SET, details); + emitter.trigger(FeaturevisorEventName.CONTEXT_SET, details); // Verify both callbacks were called assertEquals(1, handledDetails.size()); @@ -88,12 +88,12 @@ public void testMultipleListeners() { // Unsubscribe one listener unsubscribe1.unsubscribe(); - assertEquals(1, emitter.getListeners().get(Emitter.EventName.CONTEXT_SET).size()); + assertEquals(1, emitter.getListeners().get(FeaturevisorEventName.CONTEXT_SET).size()); // Trigger again - Emitter.EventDetails details2 = new Emitter.EventDetails(); + FeaturevisorEventDetails details2 = new FeaturevisorEventDetails(); details2.put("test", "single"); - emitter.trigger(Emitter.EventName.CONTEXT_SET, details2); + emitter.trigger(FeaturevisorEventName.CONTEXT_SET, details2); // Verify only the remaining callback was called assertEquals(1, handledDetails.size()); // Should still be 1 @@ -108,16 +108,16 @@ public void testMultipleListeners() { @Test public void testTriggerUsesListenerSnapshot() { List calls = new ArrayList<>(); - final Emitter.UnsubscribeFunction[] unsubscribeSecond = new Emitter.UnsubscribeFunction[1]; + final FeaturevisorUnsubscribe[] unsubscribeSecond = new FeaturevisorUnsubscribe[1]; - emitter.on(Emitter.EventName.STICKY_SET, details -> { + emitter.on(FeaturevisorEventName.STICKY_SET, details -> { calls.add("first"); unsubscribeSecond[0].unsubscribe(); }); - unsubscribeSecond[0] = emitter.on(Emitter.EventName.STICKY_SET, details -> calls.add("second")); + unsubscribeSecond[0] = emitter.on(FeaturevisorEventName.STICKY_SET, details -> calls.add("second")); - emitter.trigger(Emitter.EventName.STICKY_SET); - emitter.trigger(Emitter.EventName.STICKY_SET); + emitter.trigger(FeaturevisorEventName.STICKY_SET); + emitter.trigger(FeaturevisorEventName.STICKY_SET); assertEquals(List.of("first", "second", "first"), calls); } @@ -125,10 +125,10 @@ public void testTriggerUsesListenerSnapshot() { @Test public void testTriggerWithoutDetails() { // Add a listener - emitter.on(Emitter.EventName.STICKY_SET, this::handleDetails); + emitter.on(FeaturevisorEventName.STICKY_SET, this::handleDetails); // Trigger without details - emitter.trigger(Emitter.EventName.STICKY_SET); + emitter.trigger(FeaturevisorEventName.STICKY_SET); // Verify the callback was called with empty details assertEquals(1, handledDetails.size()); @@ -138,12 +138,12 @@ public void testTriggerWithoutDetails() { @Test public void testTriggerNonExistentEvent() { // Try to trigger an event with no listeners - Emitter.EventDetails details = new Emitter.EventDetails(); + FeaturevisorEventDetails details = new FeaturevisorEventDetails(); details.put("key", "value"); // This should not throw an exception assertDoesNotThrow(() -> { - emitter.trigger(Emitter.EventName.DATAFILE_SET, details); + emitter.trigger(FeaturevisorEventName.DATAFILE_SET, details); }); // Verify no callbacks were called @@ -153,11 +153,11 @@ public void testTriggerNonExistentEvent() { @Test public void testUnsubscribeMultipleTimes() { // Add a listener - Emitter.UnsubscribeFunction unsubscribe = emitter.on(Emitter.EventName.CONTEXT_SET, this::handleDetails); + FeaturevisorUnsubscribe unsubscribe = emitter.on(FeaturevisorEventName.CONTEXT_SET, this::handleDetails); // Unsubscribe once unsubscribe.unsubscribe(); - assertEquals(0, emitter.getListeners().get(Emitter.EventName.CONTEXT_SET).size()); + assertEquals(0, emitter.getListeners().get(FeaturevisorEventName.CONTEXT_SET).size()); // Try to unsubscribe again (should be safe) assertDoesNotThrow(() -> { @@ -165,31 +165,31 @@ public void testUnsubscribeMultipleTimes() { }); // Verify still no listeners - assertEquals(0, emitter.getListeners().get(Emitter.EventName.CONTEXT_SET).size()); + assertEquals(0, emitter.getListeners().get(FeaturevisorEventName.CONTEXT_SET).size()); } @Test public void testEventNameEnum() { // Test enum values - assertEquals("datafile_set", Emitter.EventName.DATAFILE_SET.getValue()); - assertEquals("context_set", Emitter.EventName.CONTEXT_SET.getValue()); - assertEquals("sticky_set", Emitter.EventName.STICKY_SET.getValue()); + assertEquals("datafile_set", FeaturevisorEventName.DATAFILE_SET.getValue()); + assertEquals("context_set", FeaturevisorEventName.CONTEXT_SET.getValue()); + assertEquals("sticky_set", FeaturevisorEventName.STICKY_SET.getValue()); // Test fromString method - assertEquals(Emitter.EventName.DATAFILE_SET, Emitter.EventName.fromString("datafile_set")); - assertEquals(Emitter.EventName.CONTEXT_SET, Emitter.EventName.fromString("context_set")); - assertEquals(Emitter.EventName.STICKY_SET, Emitter.EventName.fromString("sticky_set")); + assertEquals(FeaturevisorEventName.DATAFILE_SET, FeaturevisorEventName.fromString("datafile_set")); + assertEquals(FeaturevisorEventName.CONTEXT_SET, FeaturevisorEventName.fromString("context_set")); + assertEquals(FeaturevisorEventName.STICKY_SET, FeaturevisorEventName.fromString("sticky_set")); // Test invalid event name assertThrows(IllegalArgumentException.class, () -> { - Emitter.EventName.fromString("invalid_event"); + FeaturevisorEventName.fromString("invalid_event"); }); } @Test public void testEventDetails() { // Test EventDetails constructor - Emitter.EventDetails details = new Emitter.EventDetails(); + FeaturevisorEventDetails details = new FeaturevisorEventDetails(); details.put("string", "value"); details.put("number", 42); details.put("boolean", true); @@ -199,10 +199,10 @@ public void testEventDetails() { assertEquals(true, details.get("boolean")); // Test EventDetails with map constructor - Emitter.EventDetails original = new Emitter.EventDetails(); + FeaturevisorEventDetails original = new FeaturevisorEventDetails(); original.put("key", "value"); - Emitter.EventDetails copy = new Emitter.EventDetails(original); + FeaturevisorEventDetails copy = new FeaturevisorEventDetails(original); assertEquals("value", copy.get("key")); // Verify it's a copy, not a reference diff --git a/featurevisor-sdk/src/test/java/com/featurevisor/sdk/EvaluateDisabledTest.java b/featurevisor-sdk/src/test/java/com/featurevisor/sdk/EvaluateDisabledTest.java index ad15b32..4425bfe 100644 --- a/featurevisor-sdk/src/test/java/com/featurevisor/sdk/EvaluateDisabledTest.java +++ b/featurevisor-sdk/src/test/java/com/featurevisor/sdk/EvaluateDisabledTest.java @@ -13,13 +13,13 @@ public class EvaluateDisabledTest { - private Logger logger; - private DatafileReader datafileReader; + private DiagnosticReporter diagnostics; + private InstanceEvaluationDataProvider evaluationData; private Feature feature; @BeforeEach public void setUp() { - logger = Logger.createLogger(new Logger.CreateLoggerOptions().level(FeaturevisorLogLevel.WARN)); + diagnostics = DiagnosticReporter.createDiagnosticReporter(new DiagnosticReporter.DiagnosticReporterOptions().level(FeaturevisorLogLevel.WARN)); DatafileContent datafile = new DatafileContent(); datafile.setSchemaVersion("2.0"); @@ -27,9 +27,9 @@ public void setUp() { datafile.setSegments(new HashMap<>()); datafile.setFeatures(new HashMap<>()); - datafileReader = new DatafileReader(new DatafileReader.DatafileReaderOptions() + evaluationData = new InstanceEvaluationDataProvider(new InstanceEvaluationDataProvider.InstanceEvaluationDataProviderOptions() .datafile(datafile) - .logger(logger)); + .diagnostics(diagnostics)); // Create a test feature feature = new Feature("test-feature"); @@ -56,8 +56,8 @@ public void testEvaluateDisabledWithFlagType() { EvaluateOptions options = new EvaluateOptions() .type(Evaluation.TYPE_FLAG) .featureKey("test-feature") - .datafileReader(datafileReader) - .logger(logger); + .evaluationData(evaluationData) + .diagnostics(diagnostics); Evaluation flag = new Evaluation() .type(Evaluation.TYPE_FLAG) @@ -74,8 +74,8 @@ public void testEvaluateDisabledWithEnabledFlag() { EvaluateOptions options = new EvaluateOptions() .type(Evaluation.TYPE_VARIATION) .featureKey("test-feature") - .datafileReader(datafileReader) - .logger(logger); + .evaluationData(evaluationData) + .diagnostics(diagnostics); Evaluation flag = new Evaluation() .type(Evaluation.TYPE_FLAG) @@ -101,17 +101,17 @@ public void testEvaluateDisabledWithVariationType() { features.put("test-feature", testFeature); newDatafile.setFeatures(features); - // Create a new DatafileReader with the updated datafile - DatafileReader newDatafileReader = new DatafileReader(new DatafileReader.DatafileReaderOptions() + // Create a new InstanceEvaluationDataProvider with the updated datafile + InstanceEvaluationDataProvider newInstanceEvaluationDataProvider = new InstanceEvaluationDataProvider(new InstanceEvaluationDataProvider.InstanceEvaluationDataProviderOptions() .datafile(newDatafile) - .logger(logger)); + .diagnostics(diagnostics)); // Should return disabled variation evaluation EvaluateOptions options = new EvaluateOptions() .type(Evaluation.TYPE_VARIATION) .featureKey("test-feature") - .datafileReader(newDatafileReader) - .logger(logger); + .evaluationData(newInstanceEvaluationDataProvider) + .diagnostics(diagnostics); Evaluation flag = new Evaluation() .type(Evaluation.TYPE_FLAG) @@ -155,18 +155,18 @@ public void testEvaluateDisabledWithVariableTypeAndDisabledValue() { features.put("test-feature", testFeature); newDatafile.setFeatures(features); - // Create a new DatafileReader with the updated datafile - DatafileReader newDatafileReader = new DatafileReader(new DatafileReader.DatafileReaderOptions() + // Create a new InstanceEvaluationDataProvider with the updated datafile + InstanceEvaluationDataProvider newInstanceEvaluationDataProvider = new InstanceEvaluationDataProvider(new InstanceEvaluationDataProvider.InstanceEvaluationDataProviderOptions() .datafile(newDatafile) - .logger(logger)); + .diagnostics(diagnostics)); // Should return variable disabled evaluation with disabledValue EvaluateOptions options = new EvaluateOptions() .type(Evaluation.TYPE_VARIABLE) .featureKey("test-feature") .variableKey("test-variable") - .datafileReader(newDatafileReader) - .logger(logger); + .evaluationData(newInstanceEvaluationDataProvider) + .diagnostics(diagnostics); Evaluation flag = new Evaluation() .type(Evaluation.TYPE_FLAG) @@ -212,17 +212,17 @@ public void testEvaluateDisabledWithVariableTypeAndUseDefaultWhenDisabled() { features.put("test-feature", testFeature); newDatafile.setFeatures(features); - // Create a new DatafileReader with the updated datafile - DatafileReader newDatafileReader = new DatafileReader(new DatafileReader.DatafileReaderOptions() + // Create a new InstanceEvaluationDataProvider with the updated datafile + InstanceEvaluationDataProvider newInstanceEvaluationDataProvider = new InstanceEvaluationDataProvider(new InstanceEvaluationDataProvider.InstanceEvaluationDataProviderOptions() .datafile(newDatafile) - .logger(logger)); + .diagnostics(diagnostics)); EvaluateOptions options = new EvaluateOptions() .type(Evaluation.TYPE_VARIABLE) .featureKey("test-feature") .variableKey("test-variable") - .datafileReader(newDatafileReader) - .logger(logger); + .evaluationData(newInstanceEvaluationDataProvider) + .diagnostics(diagnostics); Evaluation flag = new Evaluation() .type(Evaluation.TYPE_FLAG) @@ -267,17 +267,17 @@ public void testEvaluateDisabledWithVariableTypeAndNoSpecialHandling() { features.put("test-feature", testFeature); newDatafile.setFeatures(features); - // Create a new DatafileReader with the updated datafile - DatafileReader newDatafileReader = new DatafileReader(new DatafileReader.DatafileReaderOptions() + // Create a new InstanceEvaluationDataProvider with the updated datafile + InstanceEvaluationDataProvider newInstanceEvaluationDataProvider = new InstanceEvaluationDataProvider(new InstanceEvaluationDataProvider.InstanceEvaluationDataProviderOptions() .datafile(newDatafile) - .logger(logger)); + .diagnostics(diagnostics)); EvaluateOptions options = new EvaluateOptions() .type(Evaluation.TYPE_VARIABLE) .featureKey("test-feature") .variableKey("test-variable") - .datafileReader(newDatafileReader) - .logger(logger); + .evaluationData(newInstanceEvaluationDataProvider) + .diagnostics(diagnostics); Evaluation flag = new Evaluation() .type(Evaluation.TYPE_FLAG) @@ -299,8 +299,8 @@ public void testEvaluateDisabledWithNullFlag() { EvaluateOptions options = new EvaluateOptions() .type(Evaluation.TYPE_VARIATION) .featureKey("test-feature") - .datafileReader(datafileReader) - .logger(logger); + .evaluationData(evaluationData) + .diagnostics(diagnostics); Evaluation result = EvaluateDisabled.evaluateDisabled(options, null); assertNull(result); diff --git a/featurevisor-sdk/src/test/java/com/featurevisor/sdk/EvaluateForcedTest.java b/featurevisor-sdk/src/test/java/com/featurevisor/sdk/EvaluateForcedTest.java index 51703bd..2626755 100644 --- a/featurevisor-sdk/src/test/java/com/featurevisor/sdk/EvaluateForcedTest.java +++ b/featurevisor-sdk/src/test/java/com/featurevisor/sdk/EvaluateForcedTest.java @@ -17,13 +17,13 @@ public class EvaluateForcedTest { - private Logger logger; - private DatafileReader datafileReader; + private DiagnosticReporter diagnostics; + private InstanceEvaluationDataProvider evaluationData; private Feature feature; @BeforeEach public void setUp() { - logger = Logger.createLogger(new Logger.CreateLoggerOptions().level(FeaturevisorLogLevel.WARN)); + diagnostics = DiagnosticReporter.createDiagnosticReporter(new DiagnosticReporter.DiagnosticReporterOptions().level(FeaturevisorLogLevel.WARN)); DatafileContent datafile = new DatafileContent(); datafile.setSchemaVersion("2.0"); @@ -31,9 +31,9 @@ public void setUp() { datafile.setSegments(new HashMap<>()); datafile.setFeatures(new HashMap<>()); - datafileReader = new DatafileReader(new DatafileReader.DatafileReaderOptions() + evaluationData = new InstanceEvaluationDataProvider(new InstanceEvaluationDataProvider.InstanceEvaluationDataProviderOptions() .datafile(datafile) - .logger(logger)); + .diagnostics(diagnostics)); // Create a test feature with variations feature = new Feature("test-feature"); @@ -72,8 +72,8 @@ public void testEvaluateForcedWithNoForce() { .type(Evaluation.TYPE_FLAG) .featureKey("test-feature") .context(Map.of("browser_type", "chrome")) - .datafileReader(datafileReader) - .logger(logger); + .evaluationData(evaluationData) + .diagnostics(diagnostics); EvaluateForced.EvaluateForcedResult result = EvaluateForced.evaluate(options, feature, null); @@ -97,8 +97,8 @@ public void testEvaluateForcedWithFlagType() { .type(Evaluation.TYPE_FLAG) .featureKey("test-feature") .context(Map.of("browser_type", "chrome")) - .datafileReader(datafileReader) - .logger(logger); + .evaluationData(evaluationData) + .diagnostics(diagnostics); EvaluateForced.EvaluateForcedResult result = EvaluateForced.evaluate(options, feature, null); @@ -128,8 +128,8 @@ public void testEvaluateForcedWithVariationType() { .type(Evaluation.TYPE_VARIATION) .featureKey("test-feature") .context(Map.of("browser_type", "chrome")) - .datafileReader(datafileReader) - .logger(logger); + .evaluationData(evaluationData) + .diagnostics(diagnostics); EvaluateForced.EvaluateForcedResult result = EvaluateForced.evaluate(options, feature, null); @@ -160,8 +160,8 @@ public void testEvaluateForcedWithVariationTypeNotFound() { .type(Evaluation.TYPE_VARIATION) .featureKey("test-feature") .context(Map.of("browser_type", "chrome")) - .datafileReader(datafileReader) - .logger(logger); + .evaluationData(evaluationData) + .diagnostics(diagnostics); EvaluateForced.EvaluateForcedResult result = EvaluateForced.evaluate(options, feature, null); @@ -190,8 +190,8 @@ public void testEvaluateForcedWithVariableType() { .featureKey("test-feature") .variableKey("test-variable") .context(Map.of("browser_type", "chrome")) - .datafileReader(datafileReader) - .logger(logger); + .evaluationData(evaluationData) + .diagnostics(diagnostics); EvaluateForced.EvaluateForcedResult result = EvaluateForced.evaluate(options, feature, variableSchema); @@ -228,8 +228,8 @@ public void testEvaluateForcedWithVariableTypeNotFound() { .featureKey("test-feature") .variableKey("test-variable") .context(Map.of("browser_type", "chrome")) - .datafileReader(datafileReader) - .logger(logger); + .evaluationData(evaluationData) + .diagnostics(diagnostics); EvaluateForced.EvaluateForcedResult result = EvaluateForced.evaluate(options, feature, variableSchema); @@ -253,8 +253,8 @@ public void testEvaluateForcedWithFlagTypeNoEnabled() { .type(Evaluation.TYPE_FLAG) .featureKey("test-feature") .context(Map.of("browser_type", "chrome")) - .datafileReader(datafileReader) - .logger(logger); + .evaluationData(evaluationData) + .diagnostics(diagnostics); EvaluateForced.EvaluateForcedResult result = EvaluateForced.evaluate(options, feature, null); @@ -280,8 +280,8 @@ public void testEvaluateForcedWithVariationTypeNoVariations() { .type(Evaluation.TYPE_VARIATION) .featureKey("test-feature") .context(Map.of("browser_type", "chrome")) - .datafileReader(datafileReader) - .logger(logger); + .evaluationData(evaluationData) + .diagnostics(diagnostics); EvaluateForced.EvaluateForcedResult result = EvaluateForced.evaluate(options, feature, null); @@ -308,8 +308,8 @@ public void testEvaluateForcedWithVariableTypeNoVariables() { .featureKey("test-feature") .variableKey("test-variable") .context(Map.of("browser_type", "chrome")) - .datafileReader(datafileReader) - .logger(logger); + .evaluationData(evaluationData) + .diagnostics(diagnostics); EvaluateForced.EvaluateForcedResult result = EvaluateForced.evaluate(options, feature, variableSchema); @@ -340,8 +340,8 @@ public void testEvaluateForcedWithMultipleForces() { .type(Evaluation.TYPE_FLAG) .featureKey("test-feature") .context(Map.of("browser_type", "chrome")) - .datafileReader(datafileReader) - .logger(logger); + .evaluationData(evaluationData) + .diagnostics(diagnostics); EvaluateForced.EvaluateForcedResult result = EvaluateForced.evaluate(options, feature, null); diff --git a/featurevisor-sdk/src/test/java/com/featurevisor/sdk/EvaluateNotFoundTest.java b/featurevisor-sdk/src/test/java/com/featurevisor/sdk/EvaluateNotFoundTest.java index dd2d4c1..081e5cf 100644 --- a/featurevisor-sdk/src/test/java/com/featurevisor/sdk/EvaluateNotFoundTest.java +++ b/featurevisor-sdk/src/test/java/com/featurevisor/sdk/EvaluateNotFoundTest.java @@ -16,13 +16,13 @@ public class EvaluateNotFoundTest { - private Logger logger; - private DatafileReader datafileReader; + private DiagnosticReporter diagnostics; + private InstanceEvaluationDataProvider evaluationData; private Feature feature; @BeforeEach public void setUp() { - logger = Logger.createLogger(new Logger.CreateLoggerOptions().level(FeaturevisorLogLevel.WARN)); + diagnostics = DiagnosticReporter.createDiagnosticReporter(new DiagnosticReporter.DiagnosticReporterOptions().level(FeaturevisorLogLevel.WARN)); // Create a test feature with variations feature = new Feature("test-feature"); @@ -59,10 +59,10 @@ public void setUp() { features.put("test-feature", feature); datafile.setFeatures(features); - // Create DatafileReader with the populated datafile - datafileReader = new DatafileReader(new DatafileReader.DatafileReaderOptions() + // Create InstanceEvaluationDataProvider with the populated datafile + evaluationData = new InstanceEvaluationDataProvider(new InstanceEvaluationDataProvider.InstanceEvaluationDataProviderOptions() .datafile(datafile) - .logger(logger)); + .diagnostics(diagnostics)); } @Test @@ -71,8 +71,8 @@ public void testEvaluateNotFoundWithFeatureNotFound() { EvaluateOptions options = new EvaluateOptions() .type(Evaluation.TYPE_FLAG) .featureKey("non-existent-feature") - .datafileReader(datafileReader) - .logger(logger); + .evaluationData(evaluationData) + .diagnostics(diagnostics); EvaluateNotFound.EvaluateNotFoundResult result = EvaluateNotFound.evaluateNotFound(options); @@ -91,8 +91,8 @@ public void testEvaluateNotFoundWithFeatureFound() { EvaluateOptions options = new EvaluateOptions() .type(Evaluation.TYPE_FLAG) .featureKey("test-feature") - .datafileReader(datafileReader) - .logger(logger); + .evaluationData(evaluationData) + .diagnostics(diagnostics); EvaluateNotFound.EvaluateNotFoundResult result = EvaluateNotFound.evaluateNotFound(options); @@ -111,8 +111,8 @@ public void testEvaluateNotFoundWithDeprecatedFeature() { EvaluateOptions options = new EvaluateOptions() .type(Evaluation.TYPE_FLAG) .featureKey("test-feature") - .datafileReader(datafileReader) - .logger(logger); + .evaluationData(evaluationData) + .diagnostics(diagnostics); EvaluateNotFound.EvaluateNotFoundResult result = EvaluateNotFound.evaluateNotFound(options); @@ -130,8 +130,8 @@ public void testEvaluateNotFoundWithVariableNotFound() { .type(Evaluation.TYPE_VARIABLE) .featureKey("test-feature") .variableKey("non-existent-variable") - .datafileReader(datafileReader) - .logger(logger); + .evaluationData(evaluationData) + .diagnostics(diagnostics); EvaluateNotFound.EvaluateNotFoundResult result = EvaluateNotFound.evaluateNotFound(options); @@ -152,8 +152,8 @@ public void testEvaluateNotFoundWithVariableFound() { .type(Evaluation.TYPE_VARIABLE) .featureKey("test-feature") .variableKey("test-variable") - .datafileReader(datafileReader) - .logger(logger); + .evaluationData(evaluationData) + .diagnostics(diagnostics); EvaluateNotFound.EvaluateNotFoundResult result = EvaluateNotFound.evaluateNotFound(options); @@ -174,8 +174,8 @@ public void testEvaluateNotFoundWithDeprecatedVariable() { .type(Evaluation.TYPE_VARIABLE) .featureKey("test-feature") .variableKey("test-variable") - .datafileReader(datafileReader) - .logger(logger); + .evaluationData(evaluationData) + .diagnostics(diagnostics); EvaluateNotFound.EvaluateNotFoundResult result = EvaluateNotFound.evaluateNotFound(options); @@ -194,8 +194,8 @@ public void testEvaluateNotFoundWithNoVariations() { EvaluateOptions options = new EvaluateOptions() .type(Evaluation.TYPE_VARIATION) .featureKey("test-feature") - .datafileReader(datafileReader) - .logger(logger); + .evaluationData(evaluationData) + .diagnostics(diagnostics); EvaluateNotFound.EvaluateNotFoundResult result = EvaluateNotFound.evaluateNotFound(options); @@ -216,8 +216,8 @@ public void testEvaluateNotFoundWithEmptyVariations() { EvaluateOptions options = new EvaluateOptions() .type(Evaluation.TYPE_VARIATION) .featureKey("test-feature") - .datafileReader(datafileReader) - .logger(logger); + .evaluationData(evaluationData) + .diagnostics(diagnostics); EvaluateNotFound.EvaluateNotFoundResult result = EvaluateNotFound.evaluateNotFound(options); @@ -236,8 +236,8 @@ public void testEvaluateNotFoundWithVariationsFound() { EvaluateOptions options = new EvaluateOptions() .type(Evaluation.TYPE_VARIATION) .featureKey("test-feature") - .datafileReader(datafileReader) - .logger(logger); + .evaluationData(evaluationData) + .diagnostics(diagnostics); EvaluateNotFound.EvaluateNotFoundResult result = EvaluateNotFound.evaluateNotFound(options); @@ -255,8 +255,8 @@ public void testEvaluateNotFoundWithNoVariableKey() { EvaluateOptions options = new EvaluateOptions() .type(Evaluation.TYPE_VARIABLE) .featureKey("test-feature") - .datafileReader(datafileReader) - .logger(logger); + .evaluationData(evaluationData) + .diagnostics(diagnostics); EvaluateNotFound.EvaluateNotFoundResult result = EvaluateNotFound.evaluateNotFound(options); @@ -275,8 +275,8 @@ public void testEvaluateNotFoundWithNoVariablesSchema() { .type(Evaluation.TYPE_VARIABLE) .featureKey("test-feature") .variableKey("test-variable") - .datafileReader(datafileReader) - .logger(logger); + .evaluationData(evaluationData) + .diagnostics(diagnostics); EvaluateNotFound.EvaluateNotFoundResult result = EvaluateNotFound.evaluateNotFound(options); diff --git a/featurevisor-sdk/src/test/java/com/featurevisor/sdk/EvaluateStickyTest.java b/featurevisor-sdk/src/test/java/com/featurevisor/sdk/EvaluateStickyTest.java index 4a412f3..3875164 100644 --- a/featurevisor-sdk/src/test/java/com/featurevisor/sdk/EvaluateStickyTest.java +++ b/featurevisor-sdk/src/test/java/com/featurevisor/sdk/EvaluateStickyTest.java @@ -9,12 +9,12 @@ public class EvaluateStickyTest { - private Logger logger; + private DiagnosticReporter diagnostics; private Map sticky; @BeforeEach public void setUp() { - logger = Logger.createLogger(new Logger.CreateLoggerOptions().level(FeaturevisorLogLevel.WARN)); + diagnostics = DiagnosticReporter.createDiagnosticReporter(new DiagnosticReporter.DiagnosticReporterOptions().level(FeaturevisorLogLevel.WARN)); sticky = new HashMap<>(); } @@ -24,7 +24,7 @@ public void testEvaluateStickyWithNoStickyData() { EvaluateOptions options = new EvaluateOptions() .type(Evaluation.TYPE_FLAG) .featureKey("test-feature") - .logger(logger); + .diagnostics(diagnostics); Evaluation result = EvaluateSticky.evaluateSticky(options); @@ -42,7 +42,7 @@ public void testEvaluateStickyWithFeatureNotFound() { .type(Evaluation.TYPE_FLAG) .featureKey("test-feature") .sticky(sticky) - .logger(logger); + .diagnostics(diagnostics); Evaluation result = EvaluateSticky.evaluateSticky(options); @@ -60,7 +60,7 @@ public void testEvaluateStickyWithFlagType() { .type(Evaluation.TYPE_FLAG) .featureKey("test-feature") .sticky(sticky) - .logger(logger); + .diagnostics(diagnostics); Evaluation result = EvaluateSticky.evaluateSticky(options); @@ -83,7 +83,7 @@ public void testEvaluateStickyWithFlagTypeDisabled() { .type(Evaluation.TYPE_FLAG) .featureKey("test-feature") .sticky(sticky) - .logger(logger); + .diagnostics(diagnostics); Evaluation result = EvaluateSticky.evaluateSticky(options); @@ -106,7 +106,7 @@ public void testEvaluateStickyWithFlagTypeNoEnabled() { .type(Evaluation.TYPE_FLAG) .featureKey("test-feature") .sticky(sticky) - .logger(logger); + .diagnostics(diagnostics); Evaluation result = EvaluateSticky.evaluateSticky(options); @@ -124,7 +124,7 @@ public void testEvaluateStickyWithVariationType() { .type(Evaluation.TYPE_VARIATION) .featureKey("test-feature") .sticky(sticky) - .logger(logger); + .diagnostics(diagnostics); Evaluation result = EvaluateSticky.evaluateSticky(options); @@ -146,7 +146,7 @@ public void testEvaluateStickyWithVariationTypeNull() { .type(Evaluation.TYPE_VARIATION) .featureKey("test-feature") .sticky(sticky) - .logger(logger); + .diagnostics(diagnostics); Evaluation result = EvaluateSticky.evaluateSticky(options); @@ -164,7 +164,7 @@ public void testEvaluateStickyWithVariationTypeNoVariation() { .type(Evaluation.TYPE_VARIATION) .featureKey("test-feature") .sticky(sticky) - .logger(logger); + .diagnostics(diagnostics); Evaluation result = EvaluateSticky.evaluateSticky(options); @@ -186,7 +186,7 @@ public void testEvaluateStickyWithVariableType() { .featureKey("test-feature") .variableKey("test-variable") .sticky(sticky) - .logger(logger); + .diagnostics(diagnostics); Evaluation result = EvaluateSticky.evaluateSticky(options); @@ -213,7 +213,7 @@ public void testEvaluateStickyWithVariableTypeNullValue() { .featureKey("test-feature") .variableKey("test-variable") .sticky(sticky) - .logger(logger); + .diagnostics(diagnostics); Evaluation result = EvaluateSticky.evaluateSticky(options); @@ -234,7 +234,7 @@ public void testEvaluateStickyWithVariableTypeNoVariableKey() { .type(Evaluation.TYPE_VARIABLE) .featureKey("test-feature") .sticky(sticky) - .logger(logger); + .diagnostics(diagnostics); Evaluation result = EvaluateSticky.evaluateSticky(options); @@ -253,7 +253,7 @@ public void testEvaluateStickyWithVariableTypeNoVariables() { .featureKey("test-feature") .variableKey("test-variable") .sticky(sticky) - .logger(logger); + .diagnostics(diagnostics); Evaluation result = EvaluateSticky.evaluateSticky(options); @@ -275,7 +275,7 @@ public void testEvaluateStickyWithVariableTypeNoVariableFound() { .featureKey("test-feature") .variableKey("test-variable") .sticky(sticky) - .logger(logger); + .diagnostics(diagnostics); Evaluation result = EvaluateSticky.evaluateSticky(options); @@ -291,7 +291,7 @@ public void testEvaluateStickyWithNonMapStickyData() { .type(Evaluation.TYPE_FLAG) .featureKey("test-feature") .sticky(sticky) - .logger(logger); + .diagnostics(diagnostics); Evaluation result = EvaluateSticky.evaluateSticky(options); @@ -310,7 +310,7 @@ public void testEvaluateStickyWithNonMapVariables() { .featureKey("test-feature") .variableKey("test-variable") .sticky(sticky) - .logger(logger); + .diagnostics(diagnostics); Evaluation result = EvaluateSticky.evaluateSticky(options); @@ -335,7 +335,7 @@ public void testEvaluateStickyWithComplexVariableValue() { .featureKey("test-feature") .variableKey("test-variable") .sticky(sticky) - .logger(logger); + .diagnostics(diagnostics); Evaluation result = EvaluateSticky.evaluateSticky(options); @@ -359,7 +359,7 @@ public void testEvaluateStickyWithNumericVariationValue() { .type(Evaluation.TYPE_VARIATION) .featureKey("test-feature") .sticky(sticky) - .logger(logger); + .diagnostics(diagnostics); Evaluation result = EvaluateSticky.evaluateSticky(options); diff --git a/featurevisor-sdk/src/test/java/com/featurevisor/sdk/EventsTest.java b/featurevisor-sdk/src/test/java/com/featurevisor/sdk/EventsTest.java index f7cb4cc..6806562 100644 --- a/featurevisor-sdk/src/test/java/com/featurevisor/sdk/EventsTest.java +++ b/featurevisor-sdk/src/test/java/com/featurevisor/sdk/EventsTest.java @@ -20,7 +20,7 @@ public void testGetParamsForStickySetEventEmptyToNew() { newStickyFeatures.put("feature3", Map.of("enabled", true)); boolean replace = true; - Emitter.EventDetails result = Events.getParamsForStickySetEvent( + FeaturevisorEventDetails result = Events.getParamsForStickySetEvent( previousStickyFeatures, newStickyFeatures, replace); @SuppressWarnings("unchecked") @@ -45,7 +45,7 @@ public void testGetParamsForStickySetEventAddChangeRemove() { boolean replace = true; - Emitter.EventDetails result = Events.getParamsForStickySetEvent( + FeaturevisorEventDetails result = Events.getParamsForStickySetEvent( previousStickyFeatures, newStickyFeatures, replace); @SuppressWarnings("unchecked") @@ -62,7 +62,7 @@ public void testGetParamsForStickySetEventAddChangeRemove() { @Test public void testGetParamsForStickySetEventWithNullInputs() { // Test with null inputs - Emitter.EventDetails result = Events.getParamsForStickySetEvent(null, null, false); + FeaturevisorEventDetails result = Events.getParamsForStickySetEvent(null, null, false); @SuppressWarnings("unchecked") List features = (List) result.get("features"); @@ -103,7 +103,7 @@ public void testGetParamsForDatafileSetEventEmptyToNew() { newDatafileContent.setFeatures(features); newDatafileContent.setSegments(new HashMap<>()); - Emitter.EventDetails result = Events.getParamsForDatafileSetEvent( + FeaturevisorEventDetails result = Events.getParamsForDatafileSetEvent( previousDatafileContent, newDatafileContent, false); String revision = (String) result.get("revision"); @@ -173,7 +173,7 @@ public void testGetParamsForDatafileSetEventChangeHashAddition() { newDatafileContent.setFeatures(newFeatures); newDatafileContent.setSegments(new HashMap<>()); - Emitter.EventDetails result = Events.getParamsForDatafileSetEvent( + FeaturevisorEventDetails result = Events.getParamsForDatafileSetEvent( previousDatafileContent, newDatafileContent, true); String revision = (String) result.get("revision"); @@ -233,7 +233,7 @@ public void testGetParamsForDatafileSetEventChangeHashRemoval() { newDatafileContent.setFeatures(newFeatures); newDatafileContent.setSegments(new HashMap<>()); - Emitter.EventDetails result = Events.getParamsForDatafileSetEvent( + FeaturevisorEventDetails result = Events.getParamsForDatafileSetEvent( previousDatafileContent, newDatafileContent, false); String revision = (String) result.get("revision"); @@ -253,7 +253,7 @@ public void testGetParamsForDatafileSetEventChangeHashRemoval() { @Test public void testGetParamsForDatafileSetEventWithNullInputs() { // Test with null inputs - Emitter.EventDetails result = Events.getParamsForDatafileSetEvent(null, null, false); + FeaturevisorEventDetails result = Events.getParamsForDatafileSetEvent(null, null, false); String revision = (String) result.get("revision"); String previousRevision = (String) result.get("previousRevision"); @@ -278,7 +278,7 @@ public void testGetParamsForDatafileSetEventSameRevision() { newDatafileContent.setRevision("1"); newDatafileContent.setFeatures(new HashMap<>()); - Emitter.EventDetails result = Events.getParamsForDatafileSetEvent( + FeaturevisorEventDetails result = Events.getParamsForDatafileSetEvent( previousDatafileContent, newDatafileContent, false); Boolean revisionChanged = (Boolean) result.get("revisionChanged"); diff --git a/featurevisor-sdk/src/test/java/com/featurevisor/sdk/FeaturevisorTest.java b/featurevisor-sdk/src/test/java/com/featurevisor/sdk/FeaturevisorTest.java index c0371f6..9269f23 100644 --- a/featurevisor-sdk/src/test/java/com/featurevisor/sdk/FeaturevisorTest.java +++ b/featurevisor-sdk/src/test/java/com/featurevisor/sdk/FeaturevisorTest.java @@ -26,11 +26,11 @@ public class FeaturevisorTest { - private Logger logger; + private DiagnosticReporter diagnostics; @BeforeEach public void setUp() { - logger = Logger.createLogger(new Logger.CreateLoggerOptions().level(FeaturevisorLogLevel.WARN)); + diagnostics = DiagnosticReporter.createDiagnosticReporter(new DiagnosticReporter.DiagnosticReporterOptions().level(FeaturevisorLogLevel.WARN)); } @Test @@ -46,7 +46,7 @@ public void testCreateInstanceWithNoParameters() { assertNotNull(sdk); assertEquals("2", sdk.getSchemaVersion()); - // Should have default logger and empty datafile + // Should have default diagnostics and empty datafile assertNotNull(sdk.getRevision()); assertNull(sdk.getVariation("nonExistentFeature")); } @@ -207,8 +207,8 @@ public void testCreateInstanceWithLogLevel() { ); assertNotNull(sdk); - // The logger should be set with DEBUG level - // We can't directly access the logger level, but we can verify the instance was created + // The diagnostics should be set with DEBUG level + // We can't directly access the diagnostics level, but we can verify the instance was created assertNotNull(sdk.getRevision()); } @@ -891,7 +891,7 @@ public void testSetDatafileEventIncludesReplaced() throws Exception { Featurevisor sdk = Featurevisor.createFeaturevisor(); final Object[] replaced = {null}; - sdk.on(Emitter.EventName.DATAFILE_SET, details -> replaced[0] = details.get("replaced")); + sdk.on(FeaturevisorEventName.DATAFILE_SET, details -> replaced[0] = details.get("replaced")); sdk.setDatafile(datafile, true); @@ -931,7 +931,7 @@ public void testDiagnosticHandlerFailureIsIsolated() { @Test public void testModuleCloseErrorReportsDiagnosticAndErrorEvent() { List diagnostics = new ArrayList<>(); - List errorEvents = new ArrayList<>(); + List errorEvents = new ArrayList<>(); Featurevisor sdk = Featurevisor.createFeaturevisor(new Featurevisor.FeaturevisorOptions() .onDiagnostic(diagnostics::add) @@ -940,7 +940,7 @@ public void testModuleCloseErrorReportsDiagnosticAndErrorEvent() { throw new RuntimeException("close failed"); })))); - sdk.on(Emitter.EventName.ERROR, errorEvents::add); + sdk.on(FeaturevisorEventName.ERROR, errorEvents::add); sdk.close(); assertTrue(diagnostics.stream().anyMatch(diagnostic -> @@ -989,6 +989,29 @@ public void testModuleDiagnosticSubscriptionsAndCleanup() { assertEquals(1, received.size()); } + @Test + public void testModuleDiagnosticLevelIsIndependentFromInstanceLevel() { + List received = new ArrayList<>(); + FeaturevisorModule observer = new FeaturevisorModule("observer") + .setup(api -> api.onDiagnostic( + received::add, + new FeaturevisorModuleDiagnosticOptions(FeaturevisorLogLevel.DEBUG) + )); + Featurevisor sdk = Featurevisor.createFeaturevisor(new Featurevisor.FeaturevisorOptions() + .logLevel(FeaturevisorLogLevel.FATAL) + .modules(List.of(observer))); + + sdk.isEnabled("missing"); + + FeaturevisorDiagnostic diagnostic = received.stream() + .filter(item -> "feature_not_found".equals(item.getCode())) + .findFirst() + .orElseThrow(); + assertEquals("missing", diagnostic.getDetails().get("featureKey")); + assertEquals("feature_not_found", diagnostic.getDetails().get("reason")); + assertTrue(diagnostic.getDetails().containsKey("evaluation")); + } + @Test public void testCloseClosesModules() { final boolean[] closed = {false}; diff --git a/featurevisor-sdk/src/test/java/com/featurevisor/sdk/DatafileReaderComprehensiveTest.java b/featurevisor-sdk/src/test/java/com/featurevisor/sdk/InstanceEvaluationDataProviderComprehensiveTest.java similarity index 74% rename from featurevisor-sdk/src/test/java/com/featurevisor/sdk/DatafileReaderComprehensiveTest.java rename to featurevisor-sdk/src/test/java/com/featurevisor/sdk/InstanceEvaluationDataProviderComprehensiveTest.java index ce64f9a..53d1fc6 100644 --- a/featurevisor-sdk/src/test/java/com/featurevisor/sdk/DatafileReaderComprehensiveTest.java +++ b/featurevisor-sdk/src/test/java/com/featurevisor/sdk/InstanceEvaluationDataProviderComprehensiveTest.java @@ -8,14 +8,14 @@ import java.util.*; -public class DatafileReaderComprehensiveTest { +public class InstanceEvaluationDataProviderComprehensiveTest { - private DatafileReader datafileReader; - private Logger logger; + private InstanceEvaluationDataProvider evaluationData; + private DiagnosticReporter diagnostics; @BeforeEach void setUp() { - logger = new Logger(); + diagnostics = new DiagnosticReporter(); // Create a comprehensive datafile content similar to the TypeScript tests Map datafileContent = new HashMap<>(); @@ -103,17 +103,17 @@ void setUp() { datafile.setSegments(segmentMap); datafile.setFeatures(new HashMap<>()); - datafileReader = new DatafileReader(new DatafileReader.DatafileReaderOptions() + evaluationData = new InstanceEvaluationDataProvider(new InstanceEvaluationDataProvider.InstanceEvaluationDataProviderOptions() .datafile(datafile) - .logger(logger)); + .diagnostics(diagnostics)); } @Test void testMatchEveryone() { // Test "*" segments - should match everyone - assertTrue(datafileReader.allSegmentsAreMatched("*", new HashMap<>())); - assertTrue(datafileReader.allSegmentsAreMatched("*", Map.of("foo", "foo"))); - assertTrue(datafileReader.allSegmentsAreMatched("*", Map.of("bar", "bar"))); + assertTrue(evaluationData.allSegmentsAreMatched("*", new HashMap<>())); + assertTrue(evaluationData.allSegmentsAreMatched("*", Map.of("foo", "foo"))); + assertTrue(evaluationData.allSegmentsAreMatched("*", Map.of("bar", "bar"))); } @Test @@ -122,14 +122,14 @@ void testMatchDutchMobileUsers() { List segments = Arrays.asList("mobileUsers", "netherlands"); // Should match - assertTrue(datafileReader.allSegmentsAreMatched(segments, + assertTrue(evaluationData.allSegmentsAreMatched(segments, Map.of("country", "nl", "deviceType", "mobile"))); - assertTrue(datafileReader.allSegmentsAreMatched(segments, + assertTrue(evaluationData.allSegmentsAreMatched(segments, Map.of("country", "nl", "deviceType", "mobile", "browser", "chrome"))); // Should not match - assertFalse(datafileReader.allSegmentsAreMatched(segments, new HashMap<>())); - assertFalse(datafileReader.allSegmentsAreMatched(segments, + assertFalse(evaluationData.allSegmentsAreMatched(segments, new HashMap<>())); + assertFalse(evaluationData.allSegmentsAreMatched(segments, Map.of("country", "de", "deviceType", "mobile"))); } @@ -140,14 +140,14 @@ void testMatchDutchMobileUsersWithAnd() { segments.put("and", Arrays.asList("mobileUsers", "netherlands")); // Should match - assertTrue(datafileReader.allSegmentsAreMatched(segments, + assertTrue(evaluationData.allSegmentsAreMatched(segments, Map.of("country", "nl", "deviceType", "mobile"))); - assertTrue(datafileReader.allSegmentsAreMatched(segments, + assertTrue(evaluationData.allSegmentsAreMatched(segments, Map.of("country", "nl", "deviceType", "mobile", "browser", "chrome"))); // Should not match - assertFalse(datafileReader.allSegmentsAreMatched(segments, new HashMap<>())); - assertFalse(datafileReader.allSegmentsAreMatched(segments, + assertFalse(evaluationData.allSegmentsAreMatched(segments, new HashMap<>())); + assertFalse(evaluationData.allSegmentsAreMatched(segments, Map.of("country", "de", "deviceType", "mobile"))); } @@ -158,20 +158,20 @@ void testMatchDutchMobileOrDesktopUsers() { Map.of("or", Arrays.asList("mobileUsers", "desktopUsers"))); // Should match - assertTrue(datafileReader.allSegmentsAreMatched(segments, + assertTrue(evaluationData.allSegmentsAreMatched(segments, Map.of("country", "nl", "deviceType", "mobile"))); - assertTrue(datafileReader.allSegmentsAreMatched(segments, + assertTrue(evaluationData.allSegmentsAreMatched(segments, Map.of("country", "nl", "deviceType", "mobile", "browser", "chrome"))); - assertTrue(datafileReader.allSegmentsAreMatched(segments, + assertTrue(evaluationData.allSegmentsAreMatched(segments, Map.of("country", "nl", "deviceType", "desktop"))); - assertTrue(datafileReader.allSegmentsAreMatched(segments, + assertTrue(evaluationData.allSegmentsAreMatched(segments, Map.of("country", "nl", "deviceType", "desktop", "browser", "chrome"))); // Should not match - assertFalse(datafileReader.allSegmentsAreMatched(segments, new HashMap<>())); - assertFalse(datafileReader.allSegmentsAreMatched(segments, + assertFalse(evaluationData.allSegmentsAreMatched(segments, new HashMap<>())); + assertFalse(evaluationData.allSegmentsAreMatched(segments, Map.of("country", "de", "deviceType", "mobile"))); - assertFalse(datafileReader.allSegmentsAreMatched(segments, + assertFalse(evaluationData.allSegmentsAreMatched(segments, Map.of("country", "de", "deviceType", "desktop"))); } @@ -183,14 +183,14 @@ void testMatchGermanMobileUsers() { ); // Should match - assertTrue(datafileReader.allSegmentsAreMatched(segments, + assertTrue(evaluationData.allSegmentsAreMatched(segments, Map.of("country", "de", "deviceType", "mobile"))); - assertTrue(datafileReader.allSegmentsAreMatched(segments, + assertTrue(evaluationData.allSegmentsAreMatched(segments, Map.of("country", "de", "deviceType", "mobile", "browser", "chrome"))); // Should not match - assertFalse(datafileReader.allSegmentsAreMatched(segments, new HashMap<>())); - assertFalse(datafileReader.allSegmentsAreMatched(segments, + assertFalse(evaluationData.allSegmentsAreMatched(segments, new HashMap<>())); + assertFalse(evaluationData.allSegmentsAreMatched(segments, Map.of("country", "nl", "deviceType", "mobile"))); } @@ -202,14 +202,14 @@ void testMatchGermanNonMobileUsers() { ); // Should match - assertTrue(datafileReader.allSegmentsAreMatched(segments, + assertTrue(evaluationData.allSegmentsAreMatched(segments, Map.of("country", "de", "deviceType", "desktop"))); - assertTrue(datafileReader.allSegmentsAreMatched(segments, + assertTrue(evaluationData.allSegmentsAreMatched(segments, Map.of("country", "de", "deviceType", "desktop", "browser", "chrome"))); // Should not match - assertFalse(datafileReader.allSegmentsAreMatched(segments, new HashMap<>())); - assertFalse(datafileReader.allSegmentsAreMatched(segments, + assertFalse(evaluationData.allSegmentsAreMatched(segments, new HashMap<>())); + assertFalse(evaluationData.allSegmentsAreMatched(segments, Map.of("country", "nl", "deviceType", "desktop"))); } @@ -221,24 +221,24 @@ void testMatchNotVersion55() { ); // Should match - assertTrue(datafileReader.allSegmentsAreMatched(segments, new HashMap<>())); - assertTrue(datafileReader.allSegmentsAreMatched(segments, Map.of("version", "5.6"))); - assertTrue(datafileReader.allSegmentsAreMatched(segments, Map.of("version", 5.6))); - assertTrue(datafileReader.allSegmentsAreMatched(segments, Map.of("version", "5.7"))); - assertTrue(datafileReader.allSegmentsAreMatched(segments, Map.of("version", 5.7))); + assertTrue(evaluationData.allSegmentsAreMatched(segments, new HashMap<>())); + assertTrue(evaluationData.allSegmentsAreMatched(segments, Map.of("version", "5.6"))); + assertTrue(evaluationData.allSegmentsAreMatched(segments, Map.of("version", 5.6))); + assertTrue(evaluationData.allSegmentsAreMatched(segments, Map.of("version", "5.7"))); + assertTrue(evaluationData.allSegmentsAreMatched(segments, Map.of("version", 5.7))); // Should not match - assertFalse(datafileReader.allSegmentsAreMatched(segments, Map.of("version", "5.5"))); - assertFalse(datafileReader.allSegmentsAreMatched(segments, Map.of("version", 5.5))); + assertFalse(evaluationData.allSegmentsAreMatched(segments, Map.of("version", "5.5"))); + assertFalse(evaluationData.allSegmentsAreMatched(segments, Map.of("version", 5.5))); } @Test void testNotSegmentsNegateImplicitAnd() { Map segments = Map.of("not", Arrays.asList("mobileUsers", "netherlands")); - assertFalse(datafileReader.allSegmentsAreMatched(segments, + assertFalse(evaluationData.allSegmentsAreMatched(segments, Map.of("country", "nl", "deviceType", "mobile"))); - assertTrue(datafileReader.allSegmentsAreMatched(segments, + assertTrue(evaluationData.allSegmentsAreMatched(segments, Map.of("country", "nl", "deviceType", "desktop"))); } @@ -249,19 +249,19 @@ void testNotSegmentsWithNestedOrMeanNoneMatchAndEmptyNotIsFalse() { List.of(Map.of("or", Arrays.asList("mobileUsers", "desktopUsers"))) ); - assertFalse(datafileReader.allSegmentsAreMatched(segments, Map.of("deviceType", "mobile"))); - assertTrue(datafileReader.allSegmentsAreMatched(segments, Map.of("deviceType", "tv"))); - assertFalse(datafileReader.allSegmentsAreMatched(Map.of("not", List.of()), Map.of())); + assertFalse(evaluationData.allSegmentsAreMatched(segments, Map.of("deviceType", "mobile"))); + assertTrue(evaluationData.allSegmentsAreMatched(segments, Map.of("deviceType", "tv"))); + assertFalse(evaluationData.allSegmentsAreMatched(Map.of("not", List.of()), Map.of())); } @Test void testConditionsWithWildcard() { // Test "*" conditions - should match everything - assertTrue(datafileReader.allConditionsAreMatched("*", new HashMap<>())); - assertTrue(datafileReader.allConditionsAreMatched("*", Map.of("browser_type", "chrome"))); + assertTrue(evaluationData.allConditionsAreMatched("*", new HashMap<>())); + assertTrue(evaluationData.allConditionsAreMatched("*", Map.of("browser_type", "chrome"))); // Test non-wildcard string conditions - should not match - assertFalse(datafileReader.allConditionsAreMatched("blah", Map.of("browser_type", "chrome"))); + assertFalse(evaluationData.allConditionsAreMatched("blah", Map.of("browser_type", "chrome"))); } @Test @@ -270,10 +270,10 @@ void testSimpleConditionEquals() { Map condition = Map.of("attribute", "browser_type", "operator", "equals", "value", "chrome"); // Should match - assertTrue(datafileReader.allConditionsAreMatched(condition, Map.of("browser_type", "chrome"))); + assertTrue(evaluationData.allConditionsAreMatched(condition, Map.of("browser_type", "chrome"))); // Should not match - assertFalse(datafileReader.allConditionsAreMatched(condition, Map.of("browser_type", "firefox"))); + assertFalse(evaluationData.allConditionsAreMatched(condition, Map.of("browser_type", "firefox"))); } @Test @@ -282,14 +282,14 @@ void testConditionWithDotSeparatedPath() { Map condition = Map.of("attribute", "browser.type", "operator", "equals", "value", "chrome"); // Should match - assertTrue(datafileReader.allConditionsAreMatched(condition, + assertTrue(evaluationData.allConditionsAreMatched(condition, Map.of("browser", Map.of("type", "chrome")))); // Should not match - assertFalse(datafileReader.allConditionsAreMatched(condition, + assertFalse(evaluationData.allConditionsAreMatched(condition, Map.of("browser", Map.of("type", "firefox")))); - assertFalse(datafileReader.allConditionsAreMatched(condition, + assertFalse(evaluationData.allConditionsAreMatched(condition, Map.of("browser", Map.of("blah", "firefox")))); - assertFalse(datafileReader.allConditionsAreMatched(condition, Map.of("browser", "firefox"))); + assertFalse(evaluationData.allConditionsAreMatched(condition, Map.of("browser", "firefox"))); } } diff --git a/featurevisor-sdk/src/test/java/com/featurevisor/sdk/DatafileReaderTest.java b/featurevisor-sdk/src/test/java/com/featurevisor/sdk/InstanceEvaluationDataProviderTest.java similarity index 71% rename from featurevisor-sdk/src/test/java/com/featurevisor/sdk/DatafileReaderTest.java rename to featurevisor-sdk/src/test/java/com/featurevisor/sdk/InstanceEvaluationDataProviderTest.java index de6954b..49e399e 100644 --- a/featurevisor-sdk/src/test/java/com/featurevisor/sdk/DatafileReaderTest.java +++ b/featurevisor-sdk/src/test/java/com/featurevisor/sdk/InstanceEvaluationDataProviderTest.java @@ -24,31 +24,131 @@ import java.util.HashMap; import java.util.List; import java.util.ArrayList; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; -public class DatafileReaderTest { +public class InstanceEvaluationDataProviderTest { + + @Test + public void testConcurrentConditionEvaluationsShareRegexCacheSafely() throws Exception { + DatafileContent datafile = new DatafileContent(); + datafile.setSchemaVersion("2"); + datafile.setRevision("concurrent"); + datafile.setSegments(new HashMap<>()); + datafile.setFeatures(new HashMap<>()); + InstanceEvaluationDataProvider evaluationData = new InstanceEvaluationDataProvider( + new InstanceEvaluationDataProvider.InstanceEvaluationDataProviderOptions() + .datafile(datafile) + .diagnostics(DiagnosticReporter.createDiagnosticReporter()) + ); + Condition condition = new Condition(); + condition.setAttribute("browser"); + condition.setOperator(Operator.MATCHES); + condition.setValue("^chrome$"); + condition.setRegexFlags("i"); + + ExecutorService executor = Executors.newFixedThreadPool(8); + for (int index = 0; index < 100; index++) { + executor.submit(() -> + assertTrue(evaluationData.allConditionsAreMatched(condition, Map.of("browser", "Chrome"))) + ); + } + executor.shutdown(); + assertTrue(executor.awaitTermination(10, TimeUnit.SECONDS)); + } @Test public void testSharedV3ConformanceFixture() throws Exception { try (InputStream fixtureStream = getClass().getResourceAsStream("/conformance/sdk-v3.json")) { assertNotNull(fixtureStream); - JsonNode fixture = new ObjectMapper().readTree(fixtureStream); - assertEquals(1, fixture.get("version").asInt()); + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode fixture = objectMapper.readTree(fixtureStream); + assertEquals(2, fixture.get("version").asInt()); assertEquals("control", fixture.get("bucketing").get("allocationExpectations").get("50000").asText()); assertEquals("treatment", fixture.get("bucketing").get("allocationExpectations").get("50001").asText()); + for (JsonNode testCase : fixture.get("numericBucketKeys")) { + String bucketKey = Bucketer.getBucketKey( + new Bucketer.GetBucketKeyOptions() + .featureKey("feature") + .bucketBy(new Bucket("value")) + .context(Map.of("value", testCase.get("value").doubleValue())) + .diagnostics(DiagnosticReporter.createDiagnosticReporter()) + ); + assertEquals(testCase.get("expected").asText() + ".feature", bucketKey); + } + + DatafileContent datafile = new DatafileContent(); + datafile.setSchemaVersion("2"); + datafile.setRevision("conformance"); + datafile.setSegments(new HashMap<>()); + datafile.setFeatures(new HashMap<>()); + InstanceEvaluationDataProvider evaluationData = new InstanceEvaluationDataProvider( + new InstanceEvaluationDataProvider.InstanceEvaluationDataProviderOptions() + .datafile(datafile) + .diagnostics(DiagnosticReporter.createDiagnosticReporter()) + ); + for (JsonNode testCase : fixture.get("regularExpressions").get("portableCases")) { + Condition condition = new Condition(); + condition.setAttribute("value"); + condition.setOperator(Operator.MATCHES); + condition.setValue(testCase.get("pattern").asText()); + String flags = testCase.get("flags").asText(); + condition.setRegexFlags(flags.isEmpty() ? null : flags); + assertEquals( + testCase.get("expected").asBoolean(), + evaluationData.allConditionsAreMatched( + condition, + Map.of("value", testCase.get("value").asText()) + ), + "pattern " + testCase.get("pattern").asText() + ", flags " + flags + ); + } + for (JsonNode testCase : fixture.get("conditionCases")) { + Condition condition = objectMapper.treeToValue(testCase.get("condition"), Condition.class); + @SuppressWarnings("unchecked") + Map context = objectMapper.convertValue( + testCase.get("context"), + Map.class + ); + assertEquals( + testCase.get("expected").asBoolean(), + evaluationData.allConditionsAreMatched(condition, context), + testCase.get("name").asText() + ); + } + + JsonNode aggregateCase = fixture.get("defaults").get("aggregateCase"); + DatafileContent aggregateDatafile = objectMapper.treeToValue( + aggregateCase.get("datafile"), + DatafileContent.class + ); + Featurevisor aggregateFeaturevisor = Featurevisor.createFeaturevisor( + new Featurevisor.FeaturevisorOptions().datafile(aggregateDatafile) + ); + EvaluatedFeature evaluated = aggregateFeaturevisor.getAllEvaluations( + Map.of(), + List.of(), + new Featurevisor.OverrideOptions().defaultVariationValue( + aggregateCase.get("defaultVariationValue").asText() + ) + ).getValue().get("experiment"); + assertEquals(aggregateCase.get("expected").get("enabled").asBoolean(), evaluated.getEnabled()); + assertEquals(aggregateCase.get("expected").get("variation").asText(), evaluated.getVariation()); } } - private Logger logger; + private DiagnosticReporter diagnostics; @BeforeEach public void setUp() { - logger = Logger.createLogger(new Logger.CreateLoggerOptions().level(FeaturevisorLogLevel.WARN)); + diagnostics = DiagnosticReporter.createDiagnosticReporter(new DiagnosticReporter.DiagnosticReporterOptions().level(FeaturevisorLogLevel.WARN)); } @Test - public void testDatafileReaderIsClass() { - // This test verifies that DatafileReader is a class - assertNotNull(DatafileReader.class); + public void testInstanceEvaluationDataProviderIsClass() { + // This test verifies that InstanceEvaluationDataProvider is a class + assertNotNull(InstanceEvaluationDataProvider.class); } @Test @@ -131,10 +231,10 @@ public void testV2DatafileSchemaReturnsRequestedEntities() { features.put("test", testFeature); datafileContent.setFeatures(features); - // Create DatafileReader - DatafileReader reader = new DatafileReader(new DatafileReader.DatafileReaderOptions() + // Create InstanceEvaluationDataProvider + InstanceEvaluationDataProvider reader = new InstanceEvaluationDataProvider(new InstanceEvaluationDataProvider.InstanceEvaluationDataProviderOptions() .datafile(datafileContent) - .logger(logger)); + .diagnostics(diagnostics)); // Test basic getters assertEquals("1", reader.getRevision()); @@ -224,9 +324,9 @@ public void testSegmentMatching() { datafileContent.setSegments(segments); - DatafileReader reader = new DatafileReader(new DatafileReader.DatafileReaderOptions() + InstanceEvaluationDataProvider reader = new InstanceEvaluationDataProvider(new InstanceEvaluationDataProvider.InstanceEvaluationDataProviderOptions() .datafile(datafileContent) - .logger(logger)); + .diagnostics(diagnostics)); // Test wildcard segments assertTrue(reader.allSegmentsAreMatched("*", new HashMap<>())); @@ -313,9 +413,9 @@ public void testFeatureOperations() { features.put("test", testFeature); datafileContent.setFeatures(features); - DatafileReader reader = new DatafileReader(new DatafileReader.DatafileReaderOptions() + InstanceEvaluationDataProvider reader = new InstanceEvaluationDataProvider(new InstanceEvaluationDataProvider.InstanceEvaluationDataProviderOptions() .datafile(datafileContent) - .logger(logger)); + .diagnostics(diagnostics)); // Test feature keys List featureKeys = reader.getFeatureKeys(); @@ -395,9 +495,9 @@ public void testTrafficMatching() { features.put("test", feature); datafileContent.setFeatures(features); - DatafileReader reader = new DatafileReader(new DatafileReader.DatafileReaderOptions() + InstanceEvaluationDataProvider reader = new InstanceEvaluationDataProvider(new InstanceEvaluationDataProvider.InstanceEvaluationDataProviderOptions() .datafile(datafileContent) - .logger(logger)); + .diagnostics(diagnostics)); // Test traffic matching Map netherlandsContext = Map.of("country", "nl"); diff --git a/featurevisor-sdk/src/test/java/com/featurevisor/sdk/ModulesManagerTest.java b/featurevisor-sdk/src/test/java/com/featurevisor/sdk/ModulesManagerTest.java index 859baa1..1dbfb23 100644 --- a/featurevisor-sdk/src/test/java/com/featurevisor/sdk/ModulesManagerTest.java +++ b/featurevisor-sdk/src/test/java/com/featurevisor/sdk/ModulesManagerTest.java @@ -198,8 +198,8 @@ void testBucketKeyModule() { Map context = new HashMap<>(); Bucket bucketBy = new Bucket("userId"); - ModulesManager.ConfigureBucketKeyOptions options = - new ModulesManager.ConfigureBucketKeyOptions("test-feature", context, bucketBy, "original-key"); + ConfigureBucketKeyOptions options = + new ConfigureBucketKeyOptions("test-feature", context, bucketBy, "original-key"); assertEquals("modified:original-key", modulesManager.executeBucketKeyModules(options)); } @@ -210,8 +210,8 @@ void testBucketValueModule() { .bucketValue(options -> options.getBucketValue() + 10)); Map context = new HashMap<>(); - ModulesManager.ConfigureBucketValueOptions options = - new ModulesManager.ConfigureBucketValueOptions("test-feature", "bucket-key", context, 50); + ConfigureBucketValueOptions options = + new ConfigureBucketValueOptions("test-feature", "bucket-key", context, 50); assertEquals(60, modulesManager.executeBucketValueModules(options)); } diff --git a/featurevisor-sdk/src/test/java/com/featurevisor/sdk/PublicApiTest.java b/featurevisor-sdk/src/test/java/com/featurevisor/sdk/PublicApiTest.java new file mode 100644 index 0000000..da99ddc --- /dev/null +++ b/featurevisor-sdk/src/test/java/com/featurevisor/sdk/PublicApiTest.java @@ -0,0 +1,27 @@ +package com.featurevisor.sdk; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.Modifier; +import org.junit.jupiter.api.Test; + +final class PublicApiTest { + @Test + void exposesContractsButKeepsManagersInternal() { + assertTrue(Modifier.isPublic(Featurevisor.class.getModifiers())); + assertTrue(Modifier.isPublic(ChildInstance.class.getModifiers())); + assertTrue(Modifier.isPublic(FeaturevisorModule.class.getModifiers())); + assertTrue(Modifier.isPublic(FeaturevisorEventName.class.getModifiers())); + assertTrue(Modifier.isPublic(FeaturevisorEventDetails.class.getModifiers())); + assertTrue(Modifier.isPublic(FeaturevisorEventHandler.class.getModifiers())); + assertTrue(Modifier.isPublic(FeaturevisorUnsubscribe.class.getModifiers())); + assertTrue(Modifier.isPublic(ConfigureBucketKeyOptions.class.getModifiers())); + assertTrue(Modifier.isPublic(ConfigureBucketValueOptions.class.getModifiers())); + + assertFalse(Modifier.isPublic(Emitter.class.getModifiers())); + assertFalse(Modifier.isPublic(ModulesManager.class.getModifiers())); + assertFalse(Modifier.isPublic(InstanceEvaluationDataProvider.class.getModifiers())); + assertFalse(Modifier.isPublic(Evaluate.class.getModifiers())); + } +} diff --git a/pom.xml b/pom.xml index e725fda..f1e58b1 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ - 0.0.1 + 2.0.0 11 11 15 From f775acb084908d9652e514ce0855fabd6437024f Mon Sep 17 00:00:00 2001 From: Fahad Heylaal Date: Thu, 23 Jul 2026 23:52:29 +0200 Subject: [PATCH 2/3] updates --- .../sdk/InstanceEvaluationDataProviderTest.java | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/featurevisor-sdk/src/test/java/com/featurevisor/sdk/InstanceEvaluationDataProviderTest.java b/featurevisor-sdk/src/test/java/com/featurevisor/sdk/InstanceEvaluationDataProviderTest.java index 49e399e..c38f69d 100644 --- a/featurevisor-sdk/src/test/java/com/featurevisor/sdk/InstanceEvaluationDataProviderTest.java +++ b/featurevisor-sdk/src/test/java/com/featurevisor/sdk/InstanceEvaluationDataProviderTest.java @@ -65,8 +65,6 @@ public void testSharedV3ConformanceFixture() throws Exception { ObjectMapper objectMapper = new ObjectMapper(); JsonNode fixture = objectMapper.readTree(fixtureStream); assertEquals(2, fixture.get("version").asInt()); - assertEquals("control", fixture.get("bucketing").get("allocationExpectations").get("50000").asText()); - assertEquals("treatment", fixture.get("bucketing").get("allocationExpectations").get("50001").asText()); for (JsonNode testCase : fixture.get("numericBucketKeys")) { String bucketKey = Bucketer.getBucketKey( new Bucketer.GetBucketKeyOptions() @@ -88,6 +86,19 @@ public void testSharedV3ConformanceFixture() throws Exception { .datafile(datafile) .diagnostics(DiagnosticReporter.createDiagnosticReporter()) ); + Traffic traffic = new Traffic(); + traffic.setAllocation(objectMapper.convertValue( + fixture.get("bucketing").get("allocations"), + objectMapper.getTypeFactory().constructCollectionType(List.class, Allocation.class) + )); + fixture.get("bucketing").get("allocationExpectations").fields().forEachRemaining(entry -> { + Allocation allocation = evaluationData.getMatchedAllocation( + traffic, + Integer.parseInt(entry.getKey()) + ); + assertNotNull(allocation, "bucket " + entry.getKey()); + assertEquals(entry.getValue().asText(), allocation.getVariation(), "bucket " + entry.getKey()); + }); for (JsonNode testCase : fixture.get("regularExpressions").get("portableCases")) { Condition condition = new Condition(); condition.setAttribute("value"); From cc15d27cd35550b2e1e30e8c66caf926a47e95ec Mon Sep 17 00:00:00 2001 From: Fahad Heylaal Date: Fri, 24 Jul 2026 00:10:36 +0200 Subject: [PATCH 3/3] updates --- .github/workflows/publish.yml | 8 ++++++-- README.md | 9 +++++---- .../src/main/java/com/featurevisor/cli/CLI.java | 2 +- pom.xml | 2 +- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f37e2c1..36d8519 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -36,8 +36,8 @@ jobs: id: version shell: bash run: | - if [[ ! "$GITHUB_REF_NAME" =~ ^v2\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then - echo "Expected a Featurevisor Java v2 semantic version tag" >&2 + if [[ ! "$GITHUB_REF_NAME" =~ ^v3\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then + echo "Expected a Featurevisor Java v3 semantic version tag" >&2 exit 1 fi version="${GITHUB_REF_NAME#v}" @@ -49,6 +49,10 @@ jobs: echo "README installation version does not match $GITHUB_REF_NAME" >&2 exit 1 fi + if ! grep -Fq "version = \"$version\"" featurevisor-sdk/src/main/java/com/featurevisor/cli/CLI.java; then + echo "CLI version does not match $GITHUB_REF_NAME" >&2 + exit 1 + fi echo "version=$version" >> "$GITHUB_OUTPUT" echo "Publishing Featurevisor Java $version" diff --git a/README.md b/README.md index bb30299..4828477 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ Add the Featurevisor Java SDK as a dependency with your desired version: com.featurevisor featurevisor-java - 2.0.0 + 3.0.0 ``` @@ -923,9 +923,10 @@ $ make verify-artifacts ### Releasing -- Manually create a new release on [GitHub](https://github.com/featurevisor/featurevisor-java/releases) -- Tag it with a prefix of `v`, like `v2.0.0` -- GitHub Actions publishes the parent POM, Java SDK, and OpenFeature provider to [GitHub Packages](https://github.com/orgs/featurevisor/packages?repo_name=featurevisor-java) +1. Merge the release changes into `main`. +2. Tag the release with a `v` prefix, such as `v3.0.0`, and push the tag. +3. GitHub Actions verifies and publishes the parent POM, Java SDK, and OpenFeature provider to [GitHub Packages](https://github.com/orgs/featurevisor/packages?repo_name=featurevisor-java). +4. Create the corresponding [GitHub release](https://github.com/featurevisor/featurevisor-java/releases). ## License diff --git a/featurevisor-sdk/src/main/java/com/featurevisor/cli/CLI.java b/featurevisor-sdk/src/main/java/com/featurevisor/cli/CLI.java index b13c6d2..405a3ca 100644 --- a/featurevisor-sdk/src/main/java/com/featurevisor/cli/CLI.java +++ b/featurevisor-sdk/src/main/java/com/featurevisor/cli/CLI.java @@ -33,7 +33,7 @@ @Command( name = "featurevisor", mixinStandardHelpOptions = true, - version = "2.0.0", + version = "3.0.0", description = "Featurevisor Java Library CLI - Test runner, benchmark, and distribution assessment" ) public class CLI implements Runnable { diff --git a/pom.xml b/pom.xml index f1e58b1..1164d5e 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ - 2.0.0 + 3.0.0 11 11 15