From d127cb5567bb26ae1c362e0e998e4bff0768b692 Mon Sep 17 00:00:00 2001 From: chrfwow Date: Thu, 2 Jan 2025 09:40:06 +0100 Subject: [PATCH 01/12] feat: Update in-process resolver to support flag metadata #1102 Signed-off-by: christian.lutnik --- providers/flagd/schemas | 2 +- .../resolver/process/InProcessResolver.java | 75 +++++++-- .../resolver/process/model/FeatureFlag.java | 6 +- .../process/InProcessResolverTest.java | 142 +++++++++++++----- .../flagd/resolver/process/MockFlags.java | 22 +-- .../flagd/resolver/process/TestUtils.java | 1 + .../process/model/FlagParserTest.java | 37 ++++- .../flagConfigurations/invalid-metadata.json | 21 +++ .../flagConfigurations/valid-long.json | 7 +- .../flagConfigurations/valid-simple.json | 7 +- .../FlagsmithProviderTest.java | 24 +-- 11 files changed, 264 insertions(+), 80 deletions(-) create mode 100644 providers/flagd/src/test/resources/flagConfigurations/invalid-metadata.json diff --git a/providers/flagd/schemas b/providers/flagd/schemas index b81a56eea..58aeed308 160000 --- a/providers/flagd/schemas +++ b/providers/flagd/schemas @@ -1 +1 @@ -Subproject commit b81a56eea3b2c4c543a50d4f7f79a8f32592a0af +Subproject commit 58aeed308d1a851ce47f17266128b99b28761af6 diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java index 39c77f01b..8a5fa99c8 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java @@ -2,6 +2,7 @@ import static dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag.EMPTY_TARGETING_STRING; +import java.util.Map; import java.util.function.Consumer; import java.util.function.Supplier; @@ -39,8 +40,9 @@ public class InProcessResolver implements Resolver { private final Consumer onConnectionEvent; private final Operator operator; private final long deadline; - private final ImmutableMetadata metadata; + private final ImmutableMetadata fallBackMetadata; private final Supplier connectedSupplier; + private final String scope; /** * Resolves flag values using @@ -54,16 +56,21 @@ public class InProcessResolver implements Resolver { * connection/stream */ public InProcessResolver(FlagdOptions options, final Supplier connectedSupplier, - Consumer onConnectionEvent) { + Consumer onConnectionEvent) { this.flagStore = new FlagStore(getConnector(options)); this.deadline = options.getDeadline(); this.onConnectionEvent = onConnectionEvent; this.operator = new Operator(); this.connectedSupplier = connectedSupplier; - this.metadata = options.getSelector() == null ? null - : ImmutableMetadata.builder() - .addString("scope", options.getSelector()) - .build(); + if (options.getSelector() == null) { + this.scope = null; + this.fallBackMetadata = null; + } else { + this.scope = options.getSelector(); + this.fallBackMetadata = ImmutableMetadata.builder() + .addString("scope", this.scope) + .build(); + } } /** @@ -113,8 +120,11 @@ public void shutdown() throws InterruptedException { /** * Resolve a boolean flag. */ - public ProviderEvaluation booleanEvaluation(String key, Boolean defaultValue, - EvaluationContext ctx) { + public ProviderEvaluation booleanEvaluation( + String key, + Boolean defaultValue, + EvaluationContext ctx + ) { return resolve(Boolean.class, key, ctx); } @@ -172,6 +182,7 @@ private ProviderEvaluation resolve(Class type, String key, EvaluationC return ProviderEvaluation.builder() .errorMessage("flag: " + key + " not found") .errorCode(ErrorCode.FLAG_NOT_FOUND) + .flagMetadata(fallBackMetadata) .build(); } @@ -180,6 +191,7 @@ private ProviderEvaluation resolve(Class type, String key, EvaluationC return ProviderEvaluation.builder() .errorMessage("flag: " + key + " is disabled") .errorCode(ErrorCode.FLAG_NOT_FOUND) + .flagMetadata(getFlagMetadata(flag)) .build(); } @@ -226,12 +238,51 @@ private ProviderEvaluation resolve(Class type, String key, EvaluationC throw new TypeMismatchError(message); } - final ProviderEvaluation.ProviderEvaluationBuilder evaluationBuilder = ProviderEvaluation.builder() + return ProviderEvaluation.builder() .value((T) value) .variant(resolvedVariant) - .reason(reason); + .reason(reason) + .flagMetadata(getFlagMetadata(flag)) + .build(); + } + + private ImmutableMetadata getFlagMetadata(FeatureFlag flag) { + if (flag == null) { + return fallBackMetadata; + } + + ImmutableMetadata.ImmutableMetadataBuilder metadataBuilder = ImmutableMetadata.builder(); + if (scope != null) { + metadataBuilder.addString("scope", scope); + } + + for (Map.Entry entry : flag.getMetadata().entrySet()) { + Object value = entry.getValue(); + if (value instanceof Number) { + if (value instanceof Long) { + metadataBuilder.addLong(entry.getKey(), (Long) value); + continue; + } else if (value instanceof Double) { + metadataBuilder.addDouble(entry.getKey(), (Double) value); + continue; + } else if (value instanceof Integer) { + metadataBuilder.addInteger(entry.getKey(), (Integer) value); + continue; + } else if (value instanceof Float) { + metadataBuilder.addFloat(entry.getKey(), (Float) value); + continue; + } + } else if (value instanceof Boolean) { + metadataBuilder.addBoolean(entry.getKey(), (Boolean) value); + continue; + } else if (value instanceof String) { + metadataBuilder.addString(entry.getKey(), (String) value); + continue; + } + throw new IllegalArgumentException("The type of the Metadata entry with key " + entry.getKey() + + " and value " + entry.getValue() + " is not supported"); + } - return this.metadata == null ? evaluationBuilder.build() - : evaluationBuilder.flagMetadata(this.metadata).build(); + return metadataBuilder.build(); } } diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FeatureFlag.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FeatureFlag.java index 4e687c369..b08d1e6ed 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FeatureFlag.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FeatureFlag.java @@ -25,6 +25,7 @@ public class FeatureFlag { private final String defaultVariant; private final Map variants; private final String targeting; + private final Map metadata; /** * Construct a flagd feature flag. @@ -33,11 +34,14 @@ public class FeatureFlag { public FeatureFlag(@JsonProperty("state") String state, @JsonProperty("defaultVariant") String defaultVariant, @JsonProperty("variants") Map variants, - @JsonProperty("targeting") @JsonDeserialize(using = StringSerializer.class) String targeting) { + @JsonProperty("targeting") @JsonDeserialize(using = StringSerializer.class) String targeting, + @JsonProperty("metadata") Map metadata + ) { this.state = state; this.defaultVariant = defaultVariant; this.variants = variants; this.targeting = targeting; + this.metadata = metadata; } /** diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java index 4b9bd824e..362330ae5 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java @@ -10,6 +10,8 @@ import static dev.openfeature.contrib.providers.flagd.resolver.process.MockFlags.INT_FLAG; import static dev.openfeature.contrib.providers.flagd.resolver.process.MockFlags.OBJECT_FLAG; import static dev.openfeature.contrib.providers.flagd.resolver.process.MockFlags.VARIANT_MISMATCH_FLAG; +import static dev.openfeature.contrib.providers.flagd.resolver.process.MockFlags.stringVariants; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -79,7 +81,7 @@ public void eventHandling() throws Throwable { final MutableStructure syncMetadata = new MutableStructure(); syncMetadata.add(key, val); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(new HashMap<>(), sender), + InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(new HashMap<>(), sender), (connectionEvent) -> receiver.offer(new StorageStateChange( connectionEvent.isConnected() ? StorageState.OK : StorageState.ERROR, connectionEvent.getFlagsChanged(), connectionEvent.getSyncMetadata()))); @@ -118,7 +120,7 @@ public void simpleBooleanResolving() throws Exception { final Map flagMap = new HashMap<>(); flagMap.put("booleanFlag", BOOLEAN_FLAG); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), (connectionEvent) -> { }); @@ -139,7 +141,7 @@ public void simpleDoubleResolving() throws Exception { final Map flagMap = new HashMap<>(); flagMap.put("doubleFlag", DOUBLE_FLAG); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), (connectionEvent) -> { }); @@ -159,7 +161,7 @@ public void fetchIntegerAsDouble() throws Exception { final Map flagMap = new HashMap<>(); flagMap.put("doubleFlag", DOUBLE_FLAG); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), (connectionEvent) -> { }); @@ -179,7 +181,7 @@ public void fetchDoubleAsInt() throws Exception { final Map flagMap = new HashMap<>(); flagMap.put("integerFlag", INT_FLAG); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), (connectionEvent) -> { }); @@ -199,7 +201,7 @@ public void simpleIntResolving() throws Exception { final Map flagMap = new HashMap<>(); flagMap.put("integerFlag", INT_FLAG); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), (connectionEvent) -> { }); @@ -219,7 +221,7 @@ public void simpleObjectResolving() throws Exception { final Map flagMap = new HashMap<>(); flagMap.put("objectFlag", OBJECT_FLAG); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), (connectionEvent) -> { }); @@ -246,7 +248,7 @@ public void missingFlag() throws Exception { // given final Map flagMap = new HashMap<>(); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), (connectionEvent) -> { }); @@ -262,7 +264,7 @@ public void disabledFlag() throws Exception { final Map flagMap = new HashMap<>(); flagMap.put("disabledFlag", DISABLED_FLAG); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), (connectionEvent) -> { }); @@ -278,7 +280,7 @@ public void variantMismatchFlag() throws Exception { final Map flagMap = new HashMap<>(); flagMap.put("mismatchFlag", VARIANT_MISMATCH_FLAG); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), (connectionEvent) -> { }); @@ -294,7 +296,7 @@ public void typeMismatchEvaluation() throws Exception { final Map flagMap = new HashMap<>(); flagMap.put("stringFlag", BOOLEAN_FLAG); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), (connectionEvent) -> { }); @@ -310,7 +312,7 @@ public void booleanShorthandEvaluation() throws Exception { final Map flagMap = new HashMap<>(); flagMap.put("shorthand", FLAG_WIH_SHORTHAND_TARGETING); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), (connectionEvent) -> { }); @@ -329,7 +331,7 @@ public void targetingMatchedEvaluationFlag() throws Exception { final Map flagMap = new HashMap<>(); flagMap.put("stringFlag", FLAG_WIH_IF_IN_TARGET); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), (connectionEvent) -> { }); @@ -350,7 +352,7 @@ public void targetingUnmatchedEvaluationFlag() throws Exception { final Map flagMap = new HashMap<>(); flagMap.put("stringFlag", FLAG_WIH_IF_IN_TARGET); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), (connectionEvent) -> { }); @@ -371,7 +373,7 @@ public void explicitTargetingKeyHandling() throws NoSuchFieldException, IllegalA final Map flagMap = new HashMap<>(); flagMap.put("stringFlag", FLAG_WITH_TARGETING_KEY); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), (connectionEvent) -> { }); @@ -391,7 +393,7 @@ public void targetingErrorEvaluationFlag() throws Exception { final Map flagMap = new HashMap<>(); flagMap.put("targetingErrorFlag", FLAG_WIH_INVALID_TARGET); - InProcessResolver inProcessResolver = getInProcessResolverWth(new MockStorage(flagMap), + InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), (connectionEvent) -> { }); @@ -408,7 +410,7 @@ public void validateMetadataInEvaluationResult() throws Exception { final Map flagMap = new HashMap<>(); flagMap.put("booleanFlag", BOOLEAN_FLAG); - InProcessResolver inProcessResolver = getInProcessResolverWth( + InProcessResolver inProcessResolver = getInProcessResolverWith( FlagdOptions.builder().selector(scope).build(), new MockStorage(flagMap)); @@ -423,23 +425,95 @@ public void validateMetadataInEvaluationResult() throws Exception { assertEquals(scope, metadata.getString("scope")); } - private InProcessResolver getInProcessResolverWth(final FlagdOptions options, final MockStorage storage) - throws NoSuchFieldException, IllegalAccessException { - - final InProcessResolver resolver = new InProcessResolver(options, () -> true, - (connectionEvent) -> { - }); - return injectFlagStore(resolver, storage); - } - - private InProcessResolver getInProcessResolverWth(final MockStorage storage, - final Consumer onConnectionEvent) - throws NoSuchFieldException, IllegalAccessException { - - final InProcessResolver resolver = new InProcessResolver( - FlagdOptions.builder().deadline(1000).build(), () -> true, onConnectionEvent); - return injectFlagStore(resolver, storage); - } + @Test + void selectorIsAddedToFlagMetadata() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("flag", INT_FLAG); + + InProcessResolver inProcessResolver = getInProcessResolverWith( + new MockStorage(flagMap), + connectionEvent -> { + }, + "selector"); + + // when + ProviderEvaluation providerEvaluation = inProcessResolver.integerEvaluation( + "flag", + 0, + new ImmutableContext() + ); + + // then + assertThat(providerEvaluation.getFlagMetadata()).isNotNull(); + assertThat(providerEvaluation.getFlagMetadata().getString("scope")).isEqualTo("selector"); + } + + @Test + void selectorIsOverwrittenByFlagMetadata() throws Exception { + // given + final Map flagMap = new HashMap<>(); + final Map flagMetadata = new HashMap<>(); + flagMetadata.put("scope", "new selector"); + flagMap.put( + "flag", + new FeatureFlag( + "stage", + "loop", + stringVariants, + "", + flagMetadata + ) + ); + + InProcessResolver inProcessResolver = getInProcessResolverWith( + new MockStorage(flagMap), + connectionEvent -> { + }, + "selector"); + + // when + ProviderEvaluation providerEvaluation = inProcessResolver.stringEvaluation( + "flag", + "def", + new ImmutableContext() + ); + + // then + assertThat(providerEvaluation.getFlagMetadata()).isNotNull(); + assertThat(providerEvaluation.getFlagMetadata().getString("scope")).isEqualTo("new selector"); + } + + private InProcessResolver getInProcessResolverWith(final FlagdOptions options, final MockStorage storage) + throws NoSuchFieldException, IllegalAccessException { + + final InProcessResolver resolver = new InProcessResolver( + options, + () -> true, + connectionEvent -> { + } + ); + return injectFlagStore(resolver, storage); + } + + private InProcessResolver getInProcessResolverWith(final MockStorage storage, + final Consumer onConnectionEvent) + throws NoSuchFieldException, IllegalAccessException { + + final InProcessResolver resolver = new InProcessResolver( + FlagdOptions.builder().deadline(1000).build(), () -> true, onConnectionEvent); + return injectFlagStore(resolver, storage); + } + + private InProcessResolver getInProcessResolverWith(final MockStorage storage, + final Consumer onConnectionEvent, + String selector) + throws NoSuchFieldException, IllegalAccessException { + + final InProcessResolver resolver = new InProcessResolver( + FlagdOptions.builder().selector(selector).deadline(1000).build(), () -> true, onConnectionEvent); + return injectFlagStore(resolver, storage); + } // helper to inject flagStore override private InProcessResolver injectFlagStore(final InProcessResolver resolver, final MockStorage storage) diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/MockFlags.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/MockFlags.java index 52255ea25..ce1314207 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/MockFlags.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/MockFlags.java @@ -49,38 +49,38 @@ public class MockFlags { } // correct flag - boolean - static final FeatureFlag BOOLEAN_FLAG = new FeatureFlag("ENABLED", "on", booleanVariant, null); + static final FeatureFlag BOOLEAN_FLAG = new FeatureFlag("ENABLED", "on", booleanVariant, null, new HashMap<>()); // correct flag - boolean - static final FeatureFlag SHORTHAND_FLAG = new FeatureFlag("ENABLED", "false", booleanVariant, null); + static final FeatureFlag SHORTHAND_FLAG = new FeatureFlag("ENABLED", "false", booleanVariant, null, new HashMap<>()); // correct flag - double - static final FeatureFlag DOUBLE_FLAG = new FeatureFlag("ENABLED", "one", doubleVariants, null); + static final FeatureFlag DOUBLE_FLAG = new FeatureFlag("ENABLED", "one", doubleVariants, null, new HashMap<>()); // correct flag - int - static final FeatureFlag INT_FLAG = new FeatureFlag("ENABLED", "one", intVariants, null); + static final FeatureFlag INT_FLAG = new FeatureFlag("ENABLED", "one", intVariants, null, new HashMap<>()); // correct flag - object - static final FeatureFlag OBJECT_FLAG = new FeatureFlag("ENABLED", "typeA", objectVariants, null); + static final FeatureFlag OBJECT_FLAG = new FeatureFlag("ENABLED", "typeA", objectVariants, null, new HashMap<>()); // flag in disabled state - static final FeatureFlag DISABLED_FLAG = new FeatureFlag("DISABLED", "on", booleanVariant, null); + static final FeatureFlag DISABLED_FLAG = new FeatureFlag("DISABLED", "on", booleanVariant, null, new HashMap<>()); // incorrect flag - variant mismatch - static final FeatureFlag VARIANT_MISMATCH_FLAG = new FeatureFlag("ENABLED", "true", stringVariants, null); + static final FeatureFlag VARIANT_MISMATCH_FLAG = new FeatureFlag("ENABLED", "true", stringVariants, null, new HashMap<>()); // flag with targeting rule - string static final FeatureFlag FLAG_WIH_IF_IN_TARGET = new FeatureFlag("ENABLED", "loop", stringVariants, - "{\"if\":[{\"in\":[\"@faas.com\",{\"var\":[\"email\"]}]},\"binet\",null]}"); + "{\"if\":[{\"in\":[\"@faas.com\",{\"var\":[\"email\"]}]},\"binet\",null]}", new HashMap<>()); static final FeatureFlag FLAG_WITH_TARGETING_KEY = new FeatureFlag("ENABLED", "loop", stringVariants, - "{\"if\":[{\"==\":[{\"var\":\"targetingKey\"},\"xyz\"]},\"binet\",null]}"); + "{\"if\":[{\"==\":[{\"var\":\"targetingKey\"},\"xyz\"]},\"binet\",null]}", new HashMap<>()); // flag with incorrect targeting rule static final FeatureFlag FLAG_WIH_INVALID_TARGET = new FeatureFlag("ENABLED", "loop", stringVariants, - "{if this, then that}"); + "{if this, then that}", new HashMap<>()); // flag with shorthand rule static final FeatureFlag FLAG_WIH_SHORTHAND_TARGETING = new FeatureFlag("ENABLED", "false", shorthandVariant, - "{ \"if\": [true, true, false] }"); + "{ \"if\": [true, true, false] }", new HashMap<>()); } diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/TestUtils.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/TestUtils.java index e489e6d0d..d65f557b7 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/TestUtils.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/TestUtils.java @@ -15,6 +15,7 @@ public class TestUtils { public static final String VALID_SIMPLE_EXTRA_FIELD = "flagConfigurations/valid-simple-with-extra-fields.json"; public static final String VALID_LONG = "flagConfigurations/valid-long.json"; public static final String INVALID_FLAG = "flagConfigurations/invalid-flag.json"; + public static final String INVALID_FLAG_METADATA = "flagConfigurations/invalid-metadata.json"; public static final String INVALID_CFG = "flagConfigurations/invalid-configuration.json"; public static final String UPDATABLE_FILE = "flagConfigurations/updatableFlags.json"; diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParserTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParserTest.java index 27d42dd7b..f8caace2d 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParserTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParserTest.java @@ -7,17 +7,19 @@ import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.INVALID_CFG; import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.INVALID_FLAG; +import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.INVALID_FLAG_METADATA; import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.VALID_LONG; import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.VALID_SIMPLE; import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.VALID_SIMPLE_EXTRA_FIELD; import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.getFlagsFromResource; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; class FlagParserTest { @Test - public void validJsonConfigurationParsing() throws IOException { + void validJsonConfigurationParsing() throws IOException { Map flagMap = FlagParser.parseString(getFlagsFromResource(VALID_SIMPLE), true); FeatureFlag boolFlag = flagMap.get("myBoolFlag"); @@ -29,10 +31,21 @@ public void validJsonConfigurationParsing() throws IOException { assertEquals(true, variants.get("on")); assertEquals(false, variants.get("off")); + + Map metadata = boolFlag.getMetadata(); + + assertInstanceOf(String.class, metadata.get("string")); + assertEquals("string", metadata.get("string")); + + assertInstanceOf(Boolean.class, metadata.get("boolean")); + assertEquals(true, metadata.get("boolean")); + + assertInstanceOf(Double.class, metadata.get("float")); + assertEquals(1.234, metadata.get("float")); } @Test - public void validJsonConfigurationWithExtraFieldsParsing() throws IOException { + void validJsonConfigurationWithExtraFieldsParsing() throws IOException { Map flagMap = FlagParser.parseString(getFlagsFromResource(VALID_SIMPLE_EXTRA_FIELD), true); FeatureFlag boolFlag = flagMap.get("myBoolFlag"); @@ -47,7 +60,7 @@ public void validJsonConfigurationWithExtraFieldsParsing() throws IOException { } @Test - public void validJsonConfigurationWithTargetingRulesParsing() throws IOException { + void validJsonConfigurationWithTargetingRulesParsing() throws IOException { Map flagMap = FlagParser.parseString(getFlagsFromResource(VALID_LONG), true); FeatureFlag stringFlag = flagMap.get("fibAlgo"); @@ -68,16 +81,26 @@ public void validJsonConfigurationWithTargetingRulesParsing() throws IOException @Test - public void invalidFlagThrowsError() { + void invalidFlagThrowsError() throws IOException { + String flagString = getFlagsFromResource(INVALID_FLAG); + assertThrows(IllegalArgumentException.class, () -> { + FlagParser.parseString(flagString, true); + }); + } + + @Test + void invalidFlagMetadataThrowsError() throws IOException { + String flagString = getFlagsFromResource(INVALID_FLAG_METADATA); assertThrows(IllegalArgumentException.class, () -> { - FlagParser.parseString(getFlagsFromResource(INVALID_FLAG), true); + FlagParser.parseString(flagString, true); }); } @Test - public void invalidConfigurationsThrowsError() { + void invalidConfigurationsThrowsError() throws IOException { + String flagString = getFlagsFromResource(INVALID_CFG); assertThrows(IllegalArgumentException.class, () -> { - FlagParser.parseString(getFlagsFromResource(INVALID_CFG), true); + FlagParser.parseString(flagString, true); }); } } diff --git a/providers/flagd/src/test/resources/flagConfigurations/invalid-metadata.json b/providers/flagd/src/test/resources/flagConfigurations/invalid-metadata.json new file mode 100644 index 000000000..299eac071 --- /dev/null +++ b/providers/flagd/src/test/resources/flagConfigurations/invalid-metadata.json @@ -0,0 +1,21 @@ +{ + "$schema": "../../../main/resources/flagd/schemas/flags.json", + "flags": { + "myBoolFlag": { + "state": "ENABLED", + "variants": { + "on": true, + "off": false + }, + "defaultVariant": "on", + "metadata": { + "string": "string", + "boolean": true, + "float": 1.234, + "invalid": { + "a": "a" + } + } + } + } +} diff --git a/providers/flagd/src/test/resources/flagConfigurations/valid-long.json b/providers/flagd/src/test/resources/flagConfigurations/valid-long.json index 142eec2cf..3aecd81c9 100644 --- a/providers/flagd/src/test/resources/flagConfigurations/valid-long.json +++ b/providers/flagd/src/test/resources/flagConfigurations/valid-long.json @@ -7,7 +7,12 @@ "on": true, "off": false }, - "defaultVariant": "on" + "defaultVariant": "on", + "metadata": { + "string": "string", + "boolean": true, + "float": 1.234 + } }, "myStringFlag": { "state": "ENABLED", diff --git a/providers/flagd/src/test/resources/flagConfigurations/valid-simple.json b/providers/flagd/src/test/resources/flagConfigurations/valid-simple.json index 811abbc37..37d997f3b 100644 --- a/providers/flagd/src/test/resources/flagConfigurations/valid-simple.json +++ b/providers/flagd/src/test/resources/flagConfigurations/valid-simple.json @@ -7,7 +7,12 @@ "on": true, "off": false }, - "defaultVariant": "on" + "defaultVariant": "on", + "metadata": { + "string": "string", + "boolean": true, + "float": 1.234 + } } } } diff --git a/providers/flagsmith/src/test/java/dev.openfeature.contrib.providers.flagsmith/FlagsmithProviderTest.java b/providers/flagsmith/src/test/java/dev.openfeature.contrib.providers.flagsmith/FlagsmithProviderTest.java index 74ab12d9c..85127f479 100644 --- a/providers/flagsmith/src/test/java/dev.openfeature.contrib.providers.flagsmith/FlagsmithProviderTest.java +++ b/providers/flagsmith/src/test/java/dev.openfeature.contrib.providers.flagsmith/FlagsmithProviderTest.java @@ -13,16 +13,6 @@ import dev.openfeature.sdk.Value; import dev.openfeature.sdk.exceptions.FlagNotFoundError; import dev.openfeature.sdk.exceptions.GeneralError; -import java.io.IOException; -import java.lang.reflect.Method; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.TimeUnit; -import java.util.stream.Stream; import lombok.SneakyThrows; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; @@ -35,6 +25,17 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; @@ -395,8 +396,7 @@ private String readMockResponse(String filename) throws IOException { } private String getResultString(Object responseValue, Class expectedType) - throws JsonProcessingException { - String resultString = ""; + throws JsonProcessingException { if (expectedType == Value.class) { Value value = (Value) responseValue; try { From 0bedec3d633c859d7cf0a3c197df414bff1d8caf Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Thu, 2 Jan 2025 10:18:00 +0100 Subject: [PATCH 02/12] fixup! feat: Update in-process resolver to support flag metadata #1102 Signed-off-by: christian.lutnik --- .../FlagsmithProviderTest.java | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/providers/flagsmith/src/test/java/dev.openfeature.contrib.providers.flagsmith/FlagsmithProviderTest.java b/providers/flagsmith/src/test/java/dev.openfeature.contrib.providers.flagsmith/FlagsmithProviderTest.java index 85127f479..74ab12d9c 100644 --- a/providers/flagsmith/src/test/java/dev.openfeature.contrib.providers.flagsmith/FlagsmithProviderTest.java +++ b/providers/flagsmith/src/test/java/dev.openfeature.contrib.providers.flagsmith/FlagsmithProviderTest.java @@ -13,6 +13,16 @@ import dev.openfeature.sdk.Value; import dev.openfeature.sdk.exceptions.FlagNotFoundError; import dev.openfeature.sdk.exceptions.GeneralError; +import java.io.IOException; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; import lombok.SneakyThrows; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; @@ -25,17 +35,6 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; - -import java.io.BufferedReader; -import java.io.FileReader; -import java.io.IOException; -import java.lang.reflect.Method; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.TimeUnit; -import java.util.stream.Stream; - import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; @@ -396,7 +395,8 @@ private String readMockResponse(String filename) throws IOException { } private String getResultString(Object responseValue, Class expectedType) - throws JsonProcessingException { + throws JsonProcessingException { + String resultString = ""; if (expectedType == Value.class) { Value value = (Value) responseValue; try { From c08e7bc228f1165bc049e73b6af804e886c3812b Mon Sep 17 00:00:00 2001 From: chrfwow Date: Thu, 2 Jan 2025 09:40:06 +0100 Subject: [PATCH 03/12] feat: Update in-process resolver to support flag metadata #1102 Signed-off-by: Todd Baert --- .../resolver/process/InProcessResolver.java | 87 ++- .../resolver/process/model/FeatureFlag.java | 13 +- .../process/InProcessResolverTest.java | 715 ++++++++++-------- .../flagd/resolver/process/MockFlags.java | 38 +- .../flagd/resolver/process/TestUtils.java | 1 + .../process/model/FlagParserTest.java | 37 +- .../flagConfigurations/invalid-metadata.json | 21 + .../flagConfigurations/valid-long.json | 7 +- .../flagConfigurations/valid-simple.json | 7 +- 9 files changed, 560 insertions(+), 366 deletions(-) create mode 100644 providers/flagd/src/test/resources/flagConfigurations/invalid-metadata.json diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java index 663d4bb0d..3379bb57f 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java @@ -2,6 +2,10 @@ import static dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag.EMPTY_TARGETING_STRING; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Supplier; + import dev.openfeature.contrib.providers.flagd.FlagdOptions; import dev.openfeature.contrib.providers.flagd.resolver.Resolver; import dev.openfeature.contrib.providers.flagd.resolver.common.ConnectionEvent; @@ -37,8 +41,9 @@ public class InProcessResolver implements Resolver { private final Consumer onConnectionEvent; private final Operator operator; private final long deadline; - private final ImmutableMetadata metadata; + private final ImmutableMetadata fallBackMetadata; private final Supplier connectedSupplier; + private final String scope; /** * Resolves flag values using https://buf.build/open-feature/flagd/docs/main:flagd.sync.v1. Flags @@ -48,20 +53,22 @@ public class InProcessResolver implements Resolver { * @param connectedSupplier lambda providing current connection status from caller * @param onConnectionEvent lambda which handles changes in the connection/stream */ - public InProcessResolver( - FlagdOptions options, - final Supplier connectedSupplier, - Consumer onConnectionEvent) { + public InProcessResolver(FlagdOptions options, final Supplier connectedSupplier, + Consumer onConnectionEvent) { this.flagStore = new FlagStore(getConnector(options)); this.deadline = options.getDeadline(); this.onConnectionEvent = onConnectionEvent; this.operator = new Operator(); this.connectedSupplier = connectedSupplier; - this.metadata = options.getSelector() == null - ? null - : ImmutableMetadata.builder() - .addString("scope", options.getSelector()) - .build(); + if (options.getSelector() == null) { + this.scope = null; + this.fallBackMetadata = null; + } else { + this.scope = options.getSelector(); + this.fallBackMetadata = ImmutableMetadata.builder() + .addString("scope", this.scope) + .build(); + } } /** Initialize in-process resolver. */ @@ -109,8 +116,14 @@ public void shutdown() throws InterruptedException { onConnectionEvent.accept(new ConnectionEvent(false)); } - /** Resolve a boolean flag. */ - public ProviderEvaluation booleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) { + /** + * Resolve a boolean flag. + */ + public ProviderEvaluation booleanEvaluation( + String key, + Boolean defaultValue, + EvaluationContext ctx + ) { return resolve(Boolean.class, key, ctx); } @@ -161,6 +174,7 @@ private ProviderEvaluation resolve(Class type, String key, EvaluationC return ProviderEvaluation.builder() .errorMessage("flag: " + key + " not found") .errorCode(ErrorCode.FLAG_NOT_FOUND) + .flagMetadata(fallBackMetadata) .build(); } @@ -169,6 +183,7 @@ private ProviderEvaluation resolve(Class type, String key, EvaluationC return ProviderEvaluation.builder() .errorMessage("flag: " + key + " is disabled") .errorCode(ErrorCode.FLAG_NOT_FOUND) + .flagMetadata(getFlagMetadata(flag)) .build(); } @@ -215,13 +230,51 @@ private ProviderEvaluation resolve(Class type, String key, EvaluationC throw new TypeMismatchError(message); } - final ProviderEvaluation.ProviderEvaluationBuilder evaluationBuilder = ProviderEvaluation.builder() + return ProviderEvaluation.builder() .value((T) value) .variant(resolvedVariant) - .reason(reason); + .reason(reason) + .flagMetadata(getFlagMetadata(flag)) + .build(); + } + + private ImmutableMetadata getFlagMetadata(FeatureFlag flag) { + if (flag == null) { + return fallBackMetadata; + } + + ImmutableMetadata.ImmutableMetadataBuilder metadataBuilder = ImmutableMetadata.builder(); + if (scope != null) { + metadataBuilder.addString("scope", scope); + } + + for (Map.Entry entry : flag.getMetadata().entrySet()) { + Object value = entry.getValue(); + if (value instanceof Number) { + if (value instanceof Long) { + metadataBuilder.addLong(entry.getKey(), (Long) value); + continue; + } else if (value instanceof Double) { + metadataBuilder.addDouble(entry.getKey(), (Double) value); + continue; + } else if (value instanceof Integer) { + metadataBuilder.addInteger(entry.getKey(), (Integer) value); + continue; + } else if (value instanceof Float) { + metadataBuilder.addFloat(entry.getKey(), (Float) value); + continue; + } + } else if (value instanceof Boolean) { + metadataBuilder.addBoolean(entry.getKey(), (Boolean) value); + continue; + } else if (value instanceof String) { + metadataBuilder.addString(entry.getKey(), (String) value); + continue; + } + throw new IllegalArgumentException("The type of the Metadata entry with key " + entry.getKey() + + " and value " + entry.getValue() + " is not supported"); + } - return this.metadata == null - ? evaluationBuilder.build() - : evaluationBuilder.flagMetadata(this.metadata).build(); + return metadataBuilder.build(); } } diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FeatureFlag.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FeatureFlag.java index 9a3475aa8..0f615e266 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FeatureFlag.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FeatureFlag.java @@ -23,18 +23,21 @@ public class FeatureFlag { private final String defaultVariant; private final Map variants; private final String targeting; + private final Map metadata; /** Construct a flagd feature flag. */ @JsonCreator - public FeatureFlag( - @JsonProperty("state") String state, - @JsonProperty("defaultVariant") String defaultVariant, - @JsonProperty("variants") Map variants, - @JsonProperty("targeting") @JsonDeserialize(using = StringSerializer.class) String targeting) { + public FeatureFlag(@JsonProperty("state") String state, + @JsonProperty("defaultVariant") String defaultVariant, + @JsonProperty("variants") Map variants, + @JsonProperty("targeting") @JsonDeserialize(using = StringSerializer.class) String targeting, + @JsonProperty("metadata") Map metadata + ) { this.state = state; this.defaultVariant = defaultVariant; this.variants = variants; this.targeting = targeting; + this.metadata = metadata; } /** Get targeting rule of the flag. */ diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java index 08c758c0b..362330ae5 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java @@ -10,12 +10,27 @@ import static dev.openfeature.contrib.providers.flagd.resolver.process.MockFlags.INT_FLAG; import static dev.openfeature.contrib.providers.flagd.resolver.process.MockFlags.OBJECT_FLAG; import static dev.openfeature.contrib.providers.flagd.resolver.process.MockFlags.VARIANT_MISMATCH_FLAG; +import static dev.openfeature.contrib.providers.flagd.resolver.process.MockFlags.stringVariants; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; +import java.lang.reflect.Field; +import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + import dev.openfeature.contrib.providers.flagd.Config; import dev.openfeature.contrib.providers.flagd.FlagdOptions; import dev.openfeature.contrib.providers.flagd.resolver.common.ConnectionEvent; @@ -35,406 +50,480 @@ import dev.openfeature.sdk.Value; import dev.openfeature.sdk.exceptions.ParseError; import dev.openfeature.sdk.exceptions.TypeMismatchError; -import java.lang.reflect.Field; -import java.time.Duration; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; class InProcessResolverTest { - @Test - public void connectorSetup() { - // given - FlagdOptions forGrpcOptions = FlagdOptions.builder() - .resolverType(Config.Resolver.IN_PROCESS) - .host("localhost") - .port(8080) - .build(); - FlagdOptions forOfflineOptions = FlagdOptions.builder() - .resolverType(Config.Resolver.IN_PROCESS) - .offlineFlagSourcePath("path") - .build(); - FlagdOptions forCustomConnectorOptions = FlagdOptions.builder() - .resolverType(Config.Resolver.IN_PROCESS) - .customConnector(new MockConnector(null)) - .build(); - - // then - assertInstanceOf(GrpcStreamConnector.class, InProcessResolver.getConnector(forGrpcOptions)); - assertInstanceOf(FileConnector.class, InProcessResolver.getConnector(forOfflineOptions)); - assertInstanceOf(MockConnector.class, InProcessResolver.getConnector(forCustomConnectorOptions)); - } + @Test + public void connectorSetup() { + // given + FlagdOptions forGrpcOptions = FlagdOptions.builder().resolverType(Config.Resolver.IN_PROCESS) + .host("localhost") + .port(8080).build(); + FlagdOptions forOfflineOptions = FlagdOptions.builder().resolverType(Config.Resolver.IN_PROCESS) + .offlineFlagSourcePath("path").build(); + FlagdOptions forCustomConnectorOptions = FlagdOptions.builder().resolverType(Config.Resolver.IN_PROCESS) + .customConnector(new MockConnector(null)).build(); + + // then + assertInstanceOf(GrpcStreamConnector.class, InProcessResolver.getConnector(forGrpcOptions)); + assertInstanceOf(FileConnector.class, InProcessResolver.getConnector(forOfflineOptions)); + assertInstanceOf(MockConnector.class, InProcessResolver.getConnector(forCustomConnectorOptions)); + } - @Test - public void eventHandling() throws Throwable { - // given - // note - queues with adequate capacity - final BlockingQueue sender = new LinkedBlockingQueue<>(5); - final BlockingQueue receiver = new LinkedBlockingQueue<>(5); - final String key = "key1"; - final String val = "val1"; - final MutableStructure syncMetadata = new MutableStructure(); - syncMetadata.add(key, val); - - InProcessResolver inProcessResolver = getInProcessResolverWth( - new MockStorage(new HashMap<>(), sender), - (connectionEvent) -> receiver.offer(new StorageStateChange( - connectionEvent.isConnected() ? StorageState.OK : StorageState.ERROR, - connectionEvent.getFlagsChanged(), - connectionEvent.getSyncMetadata()))); - - // when - init and emit events - Thread initThread = new Thread(() -> { - try { - inProcessResolver.init(); - } catch (Exception e) { - } - }); - initThread.start(); - if (!sender.offer( - new StorageStateChange(StorageState.OK, Collections.emptyList(), syncMetadata), - 100, - TimeUnit.MILLISECONDS)) { - Assertions.fail("failed to send the event"); + @Test + public void eventHandling() throws Throwable { + // given + // note - queues with adequate capacity + final BlockingQueue sender = new LinkedBlockingQueue<>(5); + final BlockingQueue receiver = new LinkedBlockingQueue<>(5); + final String key = "key1"; + final String val = "val1"; + final MutableStructure syncMetadata = new MutableStructure(); + syncMetadata.add(key, val); + + InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(new HashMap<>(), sender), + (connectionEvent) -> receiver.offer(new StorageStateChange( + connectionEvent.isConnected() ? StorageState.OK : StorageState.ERROR, + connectionEvent.getFlagsChanged(), connectionEvent.getSyncMetadata()))); + + // when - init and emit events + Thread initThread = new Thread(() -> { + try { + inProcessResolver.init(); + } catch (Exception e) { + } + }); + initThread.start(); + if (!sender.offer(new StorageStateChange(StorageState.OK, Collections.emptyList(), syncMetadata), 100, + TimeUnit.MILLISECONDS)) { + Assertions.fail("failed to send the event"); + } + if (!sender.offer(new StorageStateChange(StorageState.ERROR), 100, TimeUnit.MILLISECONDS)) { + Assertions.fail("failed to send the event"); + } + + // then - receive events in order + assertTimeoutPreemptively(Duration.ofMillis(200), () -> { + StorageStateChange storageState = receiver.take(); + assertEquals(StorageState.OK, storageState.getStorageState()); + assertEquals(val, storageState.getSyncMetadata().getValue(key).asString()); + }); + + assertTimeoutPreemptively(Duration.ofMillis(200), () -> { + assertEquals(StorageState.ERROR, receiver.take().getStorageState()); + }); } - if (!sender.offer(new StorageStateChange(StorageState.ERROR), 100, TimeUnit.MILLISECONDS)) { - Assertions.fail("failed to send the event"); + + @Test + public void simpleBooleanResolving() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("booleanFlag", BOOLEAN_FLAG); + + InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), + (connectionEvent) -> { + }); + + // when + ProviderEvaluation providerEvaluation = inProcessResolver.booleanEvaluation("booleanFlag", + false, + new ImmutableContext()); + + // then + assertEquals(true, providerEvaluation.getValue()); + assertEquals("on", providerEvaluation.getVariant()); + assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); } - // then - receive events in order - assertTimeoutPreemptively(Duration.ofMillis(200), () -> { - StorageStateChange storageState = receiver.take(); - assertEquals(StorageState.OK, storageState.getStorageState()); - assertEquals(val, storageState.getSyncMetadata().getValue(key).asString()); - }); + @Test + public void simpleDoubleResolving() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("doubleFlag", DOUBLE_FLAG); - assertTimeoutPreemptively(Duration.ofMillis(200), () -> { - assertEquals(StorageState.ERROR, receiver.take().getStorageState()); - }); - } + InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), + (connectionEvent) -> { + }); - @Test - public void simpleBooleanResolving() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("booleanFlag", BOOLEAN_FLAG); + // when + ProviderEvaluation providerEvaluation = inProcessResolver.doubleEvaluation("doubleFlag", 0d, + new ImmutableContext()); - InProcessResolver inProcessResolver = - getInProcessResolverWth(new MockStorage(flagMap), (connectionEvent) -> {}); + // then + assertEquals(3.141d, providerEvaluation.getValue()); + assertEquals("one", providerEvaluation.getVariant()); + assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); + } - // when - ProviderEvaluation providerEvaluation = - inProcessResolver.booleanEvaluation("booleanFlag", false, new ImmutableContext()); + @Test + public void fetchIntegerAsDouble() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("doubleFlag", DOUBLE_FLAG); - // then - assertEquals(true, providerEvaluation.getValue()); - assertEquals("on", providerEvaluation.getVariant()); - assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); - } + InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), + (connectionEvent) -> { + }); - @Test - public void simpleDoubleResolving() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("doubleFlag", DOUBLE_FLAG); + // when + ProviderEvaluation providerEvaluation = inProcessResolver.integerEvaluation("doubleFlag", 0, + new ImmutableContext()); - InProcessResolver inProcessResolver = - getInProcessResolverWth(new MockStorage(flagMap), (connectionEvent) -> {}); + // then + assertEquals(3, providerEvaluation.getValue()); + assertEquals("one", providerEvaluation.getVariant()); + assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); + } - // when - ProviderEvaluation providerEvaluation = - inProcessResolver.doubleEvaluation("doubleFlag", 0d, new ImmutableContext()); + @Test + public void fetchDoubleAsInt() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("integerFlag", INT_FLAG); - // then - assertEquals(3.141d, providerEvaluation.getValue()); - assertEquals("one", providerEvaluation.getVariant()); - assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); - } + InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), + (connectionEvent) -> { + }); - @Test - public void fetchIntegerAsDouble() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("doubleFlag", DOUBLE_FLAG); + // when + ProviderEvaluation providerEvaluation = inProcessResolver.doubleEvaluation("integerFlag", 0d, + new ImmutableContext()); - InProcessResolver inProcessResolver = - getInProcessResolverWth(new MockStorage(flagMap), (connectionEvent) -> {}); + // then + assertEquals(1d, providerEvaluation.getValue()); + assertEquals("one", providerEvaluation.getVariant()); + assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); + } - // when - ProviderEvaluation providerEvaluation = - inProcessResolver.integerEvaluation("doubleFlag", 0, new ImmutableContext()); + @Test + public void simpleIntResolving() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("integerFlag", INT_FLAG); - // then - assertEquals(3, providerEvaluation.getValue()); - assertEquals("one", providerEvaluation.getVariant()); - assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); - } + InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), + (connectionEvent) -> { + }); - @Test - public void fetchDoubleAsInt() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("integerFlag", INT_FLAG); + // when + ProviderEvaluation providerEvaluation = inProcessResolver.integerEvaluation("integerFlag", 0, + new ImmutableContext()); - InProcessResolver inProcessResolver = - getInProcessResolverWth(new MockStorage(flagMap), (connectionEvent) -> {}); + // then + assertEquals(1, providerEvaluation.getValue()); + assertEquals("one", providerEvaluation.getVariant()); + assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); + } - // when - ProviderEvaluation providerEvaluation = - inProcessResolver.doubleEvaluation("integerFlag", 0d, new ImmutableContext()); + @Test + public void simpleObjectResolving() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("objectFlag", OBJECT_FLAG); - // then - assertEquals(1d, providerEvaluation.getValue()); - assertEquals("one", providerEvaluation.getVariant()); - assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); - } + InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), + (connectionEvent) -> { + }); - @Test - public void simpleIntResolving() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("integerFlag", INT_FLAG); + Map typeDefault = new HashMap<>(); + typeDefault.put("key", "0164"); + typeDefault.put("date", "01.01.1990"); - InProcessResolver inProcessResolver = - getInProcessResolverWth(new MockStorage(flagMap), (connectionEvent) -> {}); + // when + ProviderEvaluation providerEvaluation = inProcessResolver.objectEvaluation("objectFlag", + Value.objectToValue(typeDefault), new ImmutableContext()); - // when - ProviderEvaluation providerEvaluation = - inProcessResolver.integerEvaluation("integerFlag", 0, new ImmutableContext()); + // then + Value value = providerEvaluation.getValue(); + Map valueMap = value.asStructure().asMap(); - // then - assertEquals(1, providerEvaluation.getValue()); - assertEquals("one", providerEvaluation.getVariant()); - assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); - } + assertEquals("0165", valueMap.get("key").asString()); + assertEquals("01.01.2000", valueMap.get("date").asString()); + assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); + assertEquals("typeA", providerEvaluation.getVariant()); + } - @Test - public void simpleObjectResolving() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("objectFlag", OBJECT_FLAG); + @Test + public void missingFlag() throws Exception { + // given + final Map flagMap = new HashMap<>(); - InProcessResolver inProcessResolver = - getInProcessResolverWth(new MockStorage(flagMap), (connectionEvent) -> {}); + InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), + (connectionEvent) -> { + }); - Map typeDefault = new HashMap<>(); - typeDefault.put("key", "0164"); - typeDefault.put("date", "01.01.1990"); + // when/then + ProviderEvaluation missingFlag = inProcessResolver.booleanEvaluation("missingFlag", false, + new ImmutableContext()); + assertEquals(ErrorCode.FLAG_NOT_FOUND, missingFlag.getErrorCode()); + } - // when - ProviderEvaluation providerEvaluation = inProcessResolver.objectEvaluation( - "objectFlag", Value.objectToValue(typeDefault), new ImmutableContext()); + @Test + public void disabledFlag() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("disabledFlag", DISABLED_FLAG); - // then - Value value = providerEvaluation.getValue(); - Map valueMap = value.asStructure().asMap(); + InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), + (connectionEvent) -> { + }); - assertEquals("0165", valueMap.get("key").asString()); - assertEquals("01.01.2000", valueMap.get("date").asString()); - assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); - assertEquals("typeA", providerEvaluation.getVariant()); - } + // when/then + ProviderEvaluation disabledFlag = inProcessResolver.booleanEvaluation("disabledFlag", false, + new ImmutableContext()); + assertEquals(ErrorCode.FLAG_NOT_FOUND, disabledFlag.getErrorCode()); + } - @Test - public void missingFlag() throws Exception { - // given - final Map flagMap = new HashMap<>(); + @Test + public void variantMismatchFlag() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("mismatchFlag", VARIANT_MISMATCH_FLAG); - InProcessResolver inProcessResolver = - getInProcessResolverWth(new MockStorage(flagMap), (connectionEvent) -> {}); + InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), + (connectionEvent) -> { + }); - // when/then - ProviderEvaluation missingFlag = - inProcessResolver.booleanEvaluation("missingFlag", false, new ImmutableContext()); - assertEquals(ErrorCode.FLAG_NOT_FOUND, missingFlag.getErrorCode()); - } + // when/then + assertThrows(TypeMismatchError.class, () -> { + inProcessResolver.booleanEvaluation("mismatchFlag", false, new ImmutableContext()); + }); + } - @Test - public void disabledFlag() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("disabledFlag", DISABLED_FLAG); + @Test + public void typeMismatchEvaluation() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("stringFlag", BOOLEAN_FLAG); - InProcessResolver inProcessResolver = - getInProcessResolverWth(new MockStorage(flagMap), (connectionEvent) -> {}); + InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), + (connectionEvent) -> { + }); - // when/then - ProviderEvaluation disabledFlag = - inProcessResolver.booleanEvaluation("disabledFlag", false, new ImmutableContext()); - assertEquals(ErrorCode.FLAG_NOT_FOUND, disabledFlag.getErrorCode()); - } + // when/then + assertThrows(TypeMismatchError.class, () -> { + inProcessResolver.stringEvaluation("stringFlag", "false", new ImmutableContext()); + }); + } - @Test - public void variantMismatchFlag() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("mismatchFlag", VARIANT_MISMATCH_FLAG); + @Test + public void booleanShorthandEvaluation() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("shorthand", FLAG_WIH_SHORTHAND_TARGETING); - InProcessResolver inProcessResolver = - getInProcessResolverWth(new MockStorage(flagMap), (connectionEvent) -> {}); + InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), + (connectionEvent) -> { + }); - // when/then - assertThrows(TypeMismatchError.class, () -> { - inProcessResolver.booleanEvaluation("mismatchFlag", false, new ImmutableContext()); - }); - } + ProviderEvaluation providerEvaluation = inProcessResolver.booleanEvaluation("shorthand", false, + new ImmutableContext()); - @Test - public void typeMismatchEvaluation() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("stringFlag", BOOLEAN_FLAG); + // then + assertEquals(true, providerEvaluation.getValue()); + assertEquals("true", providerEvaluation.getVariant()); + assertEquals(Reason.TARGETING_MATCH.toString(), providerEvaluation.getReason()); + } - InProcessResolver inProcessResolver = - getInProcessResolverWth(new MockStorage(flagMap), (connectionEvent) -> {}); + @Test + public void targetingMatchedEvaluationFlag() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("stringFlag", FLAG_WIH_IF_IN_TARGET); + + InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), + (connectionEvent) -> { + }); + + // when + ProviderEvaluation providerEvaluation = inProcessResolver.stringEvaluation("stringFlag", + "loopAlg", + new MutableContext().add("email", "abc@faas.com")); + + // then + assertEquals("binetAlg", providerEvaluation.getValue()); + assertEquals("binet", providerEvaluation.getVariant()); + assertEquals(Reason.TARGETING_MATCH.toString(), providerEvaluation.getReason()); + } - // when/then - assertThrows(TypeMismatchError.class, () -> { - inProcessResolver.stringEvaluation("stringFlag", "false", new ImmutableContext()); - }); - } + @Test + public void targetingUnmatchedEvaluationFlag() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("stringFlag", FLAG_WIH_IF_IN_TARGET); + + InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), + (connectionEvent) -> { + }); + + // when + ProviderEvaluation providerEvaluation = inProcessResolver.stringEvaluation("stringFlag", + "loopAlg", + new MutableContext().add("email", "abc@abc.com")); + + // then + assertEquals("loopAlg", providerEvaluation.getValue()); + assertEquals("loop", providerEvaluation.getVariant()); + assertEquals(Reason.DEFAULT.toString(), providerEvaluation.getReason()); + } - @Test - public void booleanShorthandEvaluation() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("shorthand", FLAG_WIH_SHORTHAND_TARGETING); + @Test + public void explicitTargetingKeyHandling() throws NoSuchFieldException, IllegalAccessException { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("stringFlag", FLAG_WITH_TARGETING_KEY); - InProcessResolver inProcessResolver = - getInProcessResolverWth(new MockStorage(flagMap), (connectionEvent) -> {}); + InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), + (connectionEvent) -> { + }); - ProviderEvaluation providerEvaluation = - inProcessResolver.booleanEvaluation("shorthand", false, new ImmutableContext()); + // when + ProviderEvaluation providerEvaluation = inProcessResolver.stringEvaluation("stringFlag", "loop", + new MutableContext("xyz")); - // then - assertEquals(true, providerEvaluation.getValue()); - assertEquals("true", providerEvaluation.getVariant()); - assertEquals(Reason.TARGETING_MATCH.toString(), providerEvaluation.getReason()); - } + // then + assertEquals("binetAlg", providerEvaluation.getValue()); + assertEquals("binet", providerEvaluation.getVariant()); + assertEquals(Reason.TARGETING_MATCH.toString(), providerEvaluation.getReason()); + } - @Test - public void targetingMatchedEvaluationFlag() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("stringFlag", FLAG_WIH_IF_IN_TARGET); + @Test + public void targetingErrorEvaluationFlag() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("targetingErrorFlag", FLAG_WIH_INVALID_TARGET); - InProcessResolver inProcessResolver = - getInProcessResolverWth(new MockStorage(flagMap), (connectionEvent) -> {}); + InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), + (connectionEvent) -> { + }); - // when - ProviderEvaluation providerEvaluation = inProcessResolver.stringEvaluation( - "stringFlag", "loopAlg", new MutableContext().add("email", "abc@faas.com")); + // when/then + assertThrows(ParseError.class, () -> { + inProcessResolver.booleanEvaluation("targetingErrorFlag", false, new ImmutableContext()); + }); + } - // then - assertEquals("binetAlg", providerEvaluation.getValue()); - assertEquals("binet", providerEvaluation.getVariant()); - assertEquals(Reason.TARGETING_MATCH.toString(), providerEvaluation.getReason()); - } + @Test + public void validateMetadataInEvaluationResult() throws Exception { + // given + final String scope = "appName=myApp"; + final Map flagMap = new HashMap<>(); + flagMap.put("booleanFlag", BOOLEAN_FLAG); + + InProcessResolver inProcessResolver = getInProcessResolverWith( + FlagdOptions.builder().selector(scope).build(), + new MockStorage(flagMap)); + + // when + ProviderEvaluation providerEvaluation = inProcessResolver.booleanEvaluation("booleanFlag", + false, + new ImmutableContext()); + + // then + final ImmutableMetadata metadata = providerEvaluation.getFlagMetadata(); + assertNotNull(metadata); + assertEquals(scope, metadata.getString("scope")); + } @Test - public void targetingUnmatchedEvaluationFlag() throws Exception { + void selectorIsAddedToFlagMetadata() throws Exception { // given final Map flagMap = new HashMap<>(); - flagMap.put("stringFlag", FLAG_WIH_IF_IN_TARGET); + flagMap.put("flag", INT_FLAG); - InProcessResolver inProcessResolver = - getInProcessResolverWth(new MockStorage(flagMap), (connectionEvent) -> {}); + InProcessResolver inProcessResolver = getInProcessResolverWith( + new MockStorage(flagMap), + connectionEvent -> { + }, + "selector"); // when - ProviderEvaluation providerEvaluation = inProcessResolver.stringEvaluation( - "stringFlag", "loopAlg", new MutableContext().add("email", "abc@abc.com")); + ProviderEvaluation providerEvaluation = inProcessResolver.integerEvaluation( + "flag", + 0, + new ImmutableContext() + ); // then - assertEquals("loopAlg", providerEvaluation.getValue()); - assertEquals("loop", providerEvaluation.getVariant()); - assertEquals(Reason.DEFAULT.toString(), providerEvaluation.getReason()); + assertThat(providerEvaluation.getFlagMetadata()).isNotNull(); + assertThat(providerEvaluation.getFlagMetadata().getString("scope")).isEqualTo("selector"); } @Test - public void explicitTargetingKeyHandling() throws NoSuchFieldException, IllegalAccessException { + void selectorIsOverwrittenByFlagMetadata() throws Exception { // given final Map flagMap = new HashMap<>(); - flagMap.put("stringFlag", FLAG_WITH_TARGETING_KEY); - - InProcessResolver inProcessResolver = - getInProcessResolverWth(new MockStorage(flagMap), (connectionEvent) -> {}); + final Map flagMetadata = new HashMap<>(); + flagMetadata.put("scope", "new selector"); + flagMap.put( + "flag", + new FeatureFlag( + "stage", + "loop", + stringVariants, + "", + flagMetadata + ) + ); + + InProcessResolver inProcessResolver = getInProcessResolverWith( + new MockStorage(flagMap), + connectionEvent -> { + }, + "selector"); // when - ProviderEvaluation providerEvaluation = - inProcessResolver.stringEvaluation("stringFlag", "loop", new MutableContext("xyz")); + ProviderEvaluation providerEvaluation = inProcessResolver.stringEvaluation( + "flag", + "def", + new ImmutableContext() + ); // then - assertEquals("binetAlg", providerEvaluation.getValue()); - assertEquals("binet", providerEvaluation.getVariant()); - assertEquals(Reason.TARGETING_MATCH.toString(), providerEvaluation.getReason()); - } - - @Test - public void targetingErrorEvaluationFlag() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("targetingErrorFlag", FLAG_WIH_INVALID_TARGET); - - InProcessResolver inProcessResolver = - getInProcessResolverWth(new MockStorage(flagMap), (connectionEvent) -> {}); - - // when/then - assertThrows(ParseError.class, () -> { - inProcessResolver.booleanEvaluation("targetingErrorFlag", false, new ImmutableContext()); - }); + assertThat(providerEvaluation.getFlagMetadata()).isNotNull(); + assertThat(providerEvaluation.getFlagMetadata().getString("scope")).isEqualTo("new selector"); } - @Test - public void validateMetadataInEvaluationResult() throws Exception { - // given - final String scope = "appName=myApp"; - final Map flagMap = new HashMap<>(); - flagMap.put("booleanFlag", BOOLEAN_FLAG); - - InProcessResolver inProcessResolver = - getInProcessResolverWth(FlagdOptions.builder().selector(scope).build(), new MockStorage(flagMap)); - - // when - ProviderEvaluation providerEvaluation = - inProcessResolver.booleanEvaluation("booleanFlag", false, new ImmutableContext()); + private InProcessResolver getInProcessResolverWith(final FlagdOptions options, final MockStorage storage) + throws NoSuchFieldException, IllegalAccessException { - // then - final ImmutableMetadata metadata = providerEvaluation.getFlagMetadata(); - assertNotNull(metadata); - assertEquals(scope, metadata.getString("scope")); + final InProcessResolver resolver = new InProcessResolver( + options, + () -> true, + connectionEvent -> { + } + ); + return injectFlagStore(resolver, storage); } - private InProcessResolver getInProcessResolverWth(final FlagdOptions options, final MockStorage storage) + private InProcessResolver getInProcessResolverWith(final MockStorage storage, + final Consumer onConnectionEvent) throws NoSuchFieldException, IllegalAccessException { - final InProcessResolver resolver = new InProcessResolver(options, () -> true, (connectionEvent) -> {}); + final InProcessResolver resolver = new InProcessResolver( + FlagdOptions.builder().deadline(1000).build(), () -> true, onConnectionEvent); return injectFlagStore(resolver, storage); } - private InProcessResolver getInProcessResolverWth( - final MockStorage storage, final Consumer onConnectionEvent) + private InProcessResolver getInProcessResolverWith(final MockStorage storage, + final Consumer onConnectionEvent, + String selector) throws NoSuchFieldException, IllegalAccessException { - final InProcessResolver resolver = - new InProcessResolver(FlagdOptions.builder().deadline(1000).build(), () -> true, onConnectionEvent); + final InProcessResolver resolver = new InProcessResolver( + FlagdOptions.builder().selector(selector).deadline(1000).build(), () -> true, onConnectionEvent); return injectFlagStore(resolver, storage); } - // helper to inject flagStore override - private InProcessResolver injectFlagStore(final InProcessResolver resolver, final MockStorage storage) - throws NoSuchFieldException, IllegalAccessException { + // helper to inject flagStore override + private InProcessResolver injectFlagStore(final InProcessResolver resolver, final MockStorage storage) + throws NoSuchFieldException, IllegalAccessException { - final Field flagStore = InProcessResolver.class.getDeclaredField("flagStore"); - flagStore.setAccessible(true); - flagStore.set(resolver, storage); + final Field flagStore = InProcessResolver.class.getDeclaredField("flagStore"); + flagStore.setAccessible(true); + flagStore.set(resolver, storage); + + return resolver; + } - return resolver; - } } diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/MockFlags.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/MockFlags.java index fef135cc9..e81b1fb43 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/MockFlags.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/MockFlags.java @@ -48,44 +48,38 @@ public class MockFlags { } // correct flag - boolean - static final FeatureFlag BOOLEAN_FLAG = new FeatureFlag("ENABLED", "on", booleanVariant, null); + static final FeatureFlag BOOLEAN_FLAG = new FeatureFlag("ENABLED", "on", booleanVariant, null, new HashMap<>()); // correct flag - boolean - static final FeatureFlag SHORTHAND_FLAG = new FeatureFlag("ENABLED", "false", booleanVariant, null); + static final FeatureFlag SHORTHAND_FLAG = new FeatureFlag("ENABLED", "false", booleanVariant, null, new HashMap<>()); // correct flag - double - static final FeatureFlag DOUBLE_FLAG = new FeatureFlag("ENABLED", "one", doubleVariants, null); + static final FeatureFlag DOUBLE_FLAG = new FeatureFlag("ENABLED", "one", doubleVariants, null, new HashMap<>()); // correct flag - int - static final FeatureFlag INT_FLAG = new FeatureFlag("ENABLED", "one", intVariants, null); + static final FeatureFlag INT_FLAG = new FeatureFlag("ENABLED", "one", intVariants, null, new HashMap<>()); // correct flag - object - static final FeatureFlag OBJECT_FLAG = new FeatureFlag("ENABLED", "typeA", objectVariants, null); + static final FeatureFlag OBJECT_FLAG = new FeatureFlag("ENABLED", "typeA", objectVariants, null, new HashMap<>()); // flag in disabled state - static final FeatureFlag DISABLED_FLAG = new FeatureFlag("DISABLED", "on", booleanVariant, null); + static final FeatureFlag DISABLED_FLAG = new FeatureFlag("DISABLED", "on", booleanVariant, null, new HashMap<>()); // incorrect flag - variant mismatch - static final FeatureFlag VARIANT_MISMATCH_FLAG = new FeatureFlag("ENABLED", "true", stringVariants, null); + static final FeatureFlag VARIANT_MISMATCH_FLAG = new FeatureFlag("ENABLED", "true", stringVariants, null, new HashMap<>()); // flag with targeting rule - string - static final FeatureFlag FLAG_WIH_IF_IN_TARGET = new FeatureFlag( - "ENABLED", - "loop", - stringVariants, - "{\"if\":[{\"in\":[\"@faas.com\",{\"var\":[\"email\"]}]},\"binet\",null]}"); - - static final FeatureFlag FLAG_WITH_TARGETING_KEY = new FeatureFlag( - "ENABLED", - "loop", - stringVariants, - "{\"if\":[{\"==\":[{\"var\":\"targetingKey\"},\"xyz\"]},\"binet\",null]}"); + static final FeatureFlag FLAG_WIH_IF_IN_TARGET = new FeatureFlag("ENABLED", "loop", stringVariants, + "{\"if\":[{\"in\":[\"@faas.com\",{\"var\":[\"email\"]}]},\"binet\",null]}", new HashMap<>()); + + static final FeatureFlag FLAG_WITH_TARGETING_KEY = new FeatureFlag("ENABLED", "loop", stringVariants, + "{\"if\":[{\"==\":[{\"var\":\"targetingKey\"},\"xyz\"]},\"binet\",null]}", new HashMap<>()); // flag with incorrect targeting rule - static final FeatureFlag FLAG_WIH_INVALID_TARGET = - new FeatureFlag("ENABLED", "loop", stringVariants, "{if this, then that}"); + static final FeatureFlag FLAG_WIH_INVALID_TARGET = new FeatureFlag("ENABLED", "loop", stringVariants, + "{if this, then that}", new HashMap<>()); // flag with shorthand rule - static final FeatureFlag FLAG_WIH_SHORTHAND_TARGETING = - new FeatureFlag("ENABLED", "false", shorthandVariant, "{ \"if\": [true, true, false] }"); + static final FeatureFlag FLAG_WIH_SHORTHAND_TARGETING = new FeatureFlag("ENABLED", "false", shorthandVariant, + "{ \"if\": [true, true, false] }", new HashMap<>()); } diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/TestUtils.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/TestUtils.java index c0b6795c8..a603ea266 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/TestUtils.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/TestUtils.java @@ -14,6 +14,7 @@ public class TestUtils { public static final String VALID_SIMPLE_EXTRA_FIELD = "flagConfigurations/valid-simple-with-extra-fields.json"; public static final String VALID_LONG = "flagConfigurations/valid-long.json"; public static final String INVALID_FLAG = "flagConfigurations/invalid-flag.json"; + public static final String INVALID_FLAG_METADATA = "flagConfigurations/invalid-metadata.json"; public static final String INVALID_CFG = "flagConfigurations/invalid-configuration.json"; public static final String UPDATABLE_FILE = "flagConfigurations/updatableFlags.json"; diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParserTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParserTest.java index 30453a80e..8431e5b6e 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParserTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParserTest.java @@ -2,11 +2,13 @@ import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.INVALID_CFG; import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.INVALID_FLAG; +import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.INVALID_FLAG_METADATA; import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.VALID_LONG; import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.VALID_SIMPLE; import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.VALID_SIMPLE_EXTRA_FIELD; import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.getFlagsFromResource; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -16,7 +18,7 @@ class FlagParserTest { @Test - public void validJsonConfigurationParsing() throws IOException { + void validJsonConfigurationParsing() throws IOException { Map flagMap = FlagParser.parseString(getFlagsFromResource(VALID_SIMPLE), true); FeatureFlag boolFlag = flagMap.get("myBoolFlag"); @@ -28,10 +30,21 @@ public void validJsonConfigurationParsing() throws IOException { assertEquals(true, variants.get("on")); assertEquals(false, variants.get("off")); + + Map metadata = boolFlag.getMetadata(); + + assertInstanceOf(String.class, metadata.get("string")); + assertEquals("string", metadata.get("string")); + + assertInstanceOf(Boolean.class, metadata.get("boolean")); + assertEquals(true, metadata.get("boolean")); + + assertInstanceOf(Double.class, metadata.get("float")); + assertEquals(1.234, metadata.get("float")); } @Test - public void validJsonConfigurationWithExtraFieldsParsing() throws IOException { + void validJsonConfigurationWithExtraFieldsParsing() throws IOException { Map flagMap = FlagParser.parseString(getFlagsFromResource(VALID_SIMPLE_EXTRA_FIELD), true); FeatureFlag boolFlag = flagMap.get("myBoolFlag"); @@ -46,7 +59,7 @@ public void validJsonConfigurationWithExtraFieldsParsing() throws IOException { } @Test - public void validJsonConfigurationWithTargetingRulesParsing() throws IOException { + void validJsonConfigurationWithTargetingRulesParsing() throws IOException { Map flagMap = FlagParser.parseString(getFlagsFromResource(VALID_LONG), true); FeatureFlag stringFlag = flagMap.get("fibAlgo"); @@ -66,16 +79,26 @@ public void validJsonConfigurationWithTargetingRulesParsing() throws IOException } @Test - public void invalidFlagThrowsError() { + void invalidFlagThrowsError() throws IOException { + String flagString = getFlagsFromResource(INVALID_FLAG); + assertThrows(IllegalArgumentException.class, () -> { + FlagParser.parseString(flagString, true); + }); + } + + @Test + void invalidFlagMetadataThrowsError() throws IOException { + String flagString = getFlagsFromResource(INVALID_FLAG_METADATA); assertThrows(IllegalArgumentException.class, () -> { - FlagParser.parseString(getFlagsFromResource(INVALID_FLAG), true); + FlagParser.parseString(flagString, true); }); } @Test - public void invalidConfigurationsThrowsError() { + void invalidConfigurationsThrowsError() throws IOException { + String flagString = getFlagsFromResource(INVALID_CFG); assertThrows(IllegalArgumentException.class, () -> { - FlagParser.parseString(getFlagsFromResource(INVALID_CFG), true); + FlagParser.parseString(flagString, true); }); } } diff --git a/providers/flagd/src/test/resources/flagConfigurations/invalid-metadata.json b/providers/flagd/src/test/resources/flagConfigurations/invalid-metadata.json new file mode 100644 index 000000000..299eac071 --- /dev/null +++ b/providers/flagd/src/test/resources/flagConfigurations/invalid-metadata.json @@ -0,0 +1,21 @@ +{ + "$schema": "../../../main/resources/flagd/schemas/flags.json", + "flags": { + "myBoolFlag": { + "state": "ENABLED", + "variants": { + "on": true, + "off": false + }, + "defaultVariant": "on", + "metadata": { + "string": "string", + "boolean": true, + "float": 1.234, + "invalid": { + "a": "a" + } + } + } + } +} diff --git a/providers/flagd/src/test/resources/flagConfigurations/valid-long.json b/providers/flagd/src/test/resources/flagConfigurations/valid-long.json index 142eec2cf..3aecd81c9 100644 --- a/providers/flagd/src/test/resources/flagConfigurations/valid-long.json +++ b/providers/flagd/src/test/resources/flagConfigurations/valid-long.json @@ -7,7 +7,12 @@ "on": true, "off": false }, - "defaultVariant": "on" + "defaultVariant": "on", + "metadata": { + "string": "string", + "boolean": true, + "float": 1.234 + } }, "myStringFlag": { "state": "ENABLED", diff --git a/providers/flagd/src/test/resources/flagConfigurations/valid-simple.json b/providers/flagd/src/test/resources/flagConfigurations/valid-simple.json index 811abbc37..37d997f3b 100644 --- a/providers/flagd/src/test/resources/flagConfigurations/valid-simple.json +++ b/providers/flagd/src/test/resources/flagConfigurations/valid-simple.json @@ -7,7 +7,12 @@ "on": true, "off": false }, - "defaultVariant": "on" + "defaultVariant": "on", + "metadata": { + "string": "string", + "boolean": true, + "float": 1.234 + } } } } From da9e92b9f352b71ce9b79be77e7c200d97d73e9e Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Thu, 9 Jan 2025 10:48:43 +0100 Subject: [PATCH 04/12] fixup! feat: Update in-process resolver to support flag metadata #1102 Signed-off-by: christian.lutnik --- .../resolver/process/InProcessResolver.java | 21 +- .../resolver/process/model/FeatureFlag.java | 40 +- .../resolver/process/model/FlagParser.java | 18 +- .../process/InProcessResolverTest.java | 730 +++++++++--------- .../flagd/resolver/process/MockFlags.java | 32 +- .../flagd/resolver/process/TestUtils.java | 2 + .../process/model/FlagParserTest.java | 58 +- .../invalid-global-metadata.json | 21 + .../valid-global-metadata.json | 31 + 9 files changed, 526 insertions(+), 427 deletions(-) create mode 100644 providers/flagd/src/test/resources/flagConfigurations/invalid-global-metadata.json create mode 100644 providers/flagd/src/test/resources/flagConfigurations/valid-global-metadata.json diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java index 3379bb57f..163d288c0 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java @@ -2,10 +2,6 @@ import static dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag.EMPTY_TARGETING_STRING; -import java.util.Map; -import java.util.function.Consumer; -import java.util.function.Supplier; - import dev.openfeature.contrib.providers.flagd.FlagdOptions; import dev.openfeature.contrib.providers.flagd.resolver.Resolver; import dev.openfeature.contrib.providers.flagd.resolver.common.ConnectionEvent; @@ -27,6 +23,7 @@ import dev.openfeature.sdk.Value; import dev.openfeature.sdk.exceptions.ParseError; import dev.openfeature.sdk.exceptions.TypeMismatchError; +import java.util.Map; import java.util.function.Consumer; import java.util.function.Supplier; import lombok.extern.slf4j.Slf4j; @@ -53,8 +50,10 @@ public class InProcessResolver implements Resolver { * @param connectedSupplier lambda providing current connection status from caller * @param onConnectionEvent lambda which handles changes in the connection/stream */ - public InProcessResolver(FlagdOptions options, final Supplier connectedSupplier, - Consumer onConnectionEvent) { + public InProcessResolver( + FlagdOptions options, + final Supplier connectedSupplier, + Consumer onConnectionEvent) { this.flagStore = new FlagStore(getConnector(options)); this.deadline = options.getDeadline(); this.onConnectionEvent = onConnectionEvent; @@ -65,9 +64,7 @@ public InProcessResolver(FlagdOptions options, final Supplier connected this.fallBackMetadata = null; } else { this.scope = options.getSelector(); - this.fallBackMetadata = ImmutableMetadata.builder() - .addString("scope", this.scope) - .build(); + this.fallBackMetadata = ImmutableMetadata.builder().addString("scope", this.scope).build(); } } @@ -119,11 +116,7 @@ public void shutdown() throws InterruptedException { /** * Resolve a boolean flag. */ - public ProviderEvaluation booleanEvaluation( - String key, - Boolean defaultValue, - EvaluationContext ctx - ) { + public ProviderEvaluation booleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) { return resolve(Boolean.class, key, ctx); } diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FeatureFlag.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FeatureFlag.java index 0f615e266..95d46a102 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FeatureFlag.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FeatureFlag.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.util.HashMap; import java.util.Map; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -27,17 +28,42 @@ public class FeatureFlag { /** Construct a flagd feature flag. */ @JsonCreator - public FeatureFlag(@JsonProperty("state") String state, - @JsonProperty("defaultVariant") String defaultVariant, - @JsonProperty("variants") Map variants, - @JsonProperty("targeting") @JsonDeserialize(using = StringSerializer.class) String targeting, - @JsonProperty("metadata") Map metadata - ) { + public FeatureFlag( + @JsonProperty("state") String state, + @JsonProperty("defaultVariant") String defaultVariant, + @JsonProperty("variants") Map variants, + @JsonProperty("targeting") @JsonDeserialize(using = StringSerializer.class) String targeting, + @JsonProperty("metadata") Map metadata) { this.state = state; this.defaultVariant = defaultVariant; this.variants = variants; this.targeting = targeting; - this.metadata = metadata; + if (metadata == null) { + this.metadata = new HashMap<>(); + } else { + this.metadata = metadata; + } + } + + /** Construct a flagd feature flag. */ + public FeatureFlag(String state, String defaultVariant, Map variants, String targeting) { + this.state = state; + this.defaultVariant = defaultVariant; + this.variants = variants; + this.targeting = targeting; + this.metadata = new HashMap<>(); + } + + /** + * Add global metadata to this FeatureFlag. Keys that already exist in the metadata of this flag are not + * overwritten. + * + * @param metadata The metadata to add to this flag + */ + public void addMetadata(Map metadata) { + for (Map.Entry entry : metadata.entrySet()) { + this.metadata.putIfAbsent(entry.getKey(), entry.getValue()); + } } /** Get targeting rule of the flag. */ diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParser.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParser.java index 1dbe7f710..1dbbc3030 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParser.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParser.java @@ -1,7 +1,9 @@ package dev.openfeature.contrib.providers.flagd.resolver.process.model; import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.TreeNode; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.networknt.schema.JsonSchema; import com.networknt.schema.JsonSchemaFactory; @@ -24,6 +26,7 @@ justification = "Feature flag comes as a Json configuration, hence they must be exposed") public class FlagParser { private static final String FLAG_KEY = "flags"; + private static final String METADATA_KEY = "metadata"; private static final String EVALUATOR_KEY = "$evaluators"; private static final String REPLACER_FORMAT = "\"\\$ref\":(\\s)*\"%s\""; private static final ObjectMapper MAPPER = new ObjectMapper(); @@ -73,6 +76,8 @@ public static Map parseString(final String configuration, b try (JsonParser parser = MAPPER.createParser(transposedConfiguration)) { final TreeNode treeNode = parser.readValueAsTree(); final TreeNode flagNode = treeNode.get(FLAG_KEY); + final TreeNode metadataNode = treeNode.get(METADATA_KEY); + final Map metadata = parseMetadata(metadataNode); if (flagNode == null) { throw new IllegalArgumentException("No flag configurations found in the payload"); @@ -81,13 +86,24 @@ public static Map parseString(final String configuration, b final Iterator it = flagNode.fieldNames(); while (it.hasNext()) { final String key = it.next(); - flagMap.put(key, MAPPER.treeToValue(flagNode.get(key), FeatureFlag.class)); + FeatureFlag flag = MAPPER.treeToValue(flagNode.get(key), FeatureFlag.class); + flag.addMetadata(metadata); + flagMap.put(key, flag); } } return flagMap; } + private static Map parseMetadata(TreeNode metadataNode) throws JsonProcessingException { + if (metadataNode == null) { + return new HashMap<>(); + } + + TypeReference> typeRef = new TypeReference>() {}; + return MAPPER.treeToValue(metadataNode, typeRef); + } + private static String transposeEvaluators(final String configuration) throws IOException { try (JsonParser parser = MAPPER.createParser(configuration)) { final Map patternMap = new HashMap<>(); diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java index 362330ae5..9fcff9746 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java @@ -18,19 +18,6 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; -import java.lang.reflect.Field; -import java.time.Duration; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - import dev.openfeature.contrib.providers.flagd.Config; import dev.openfeature.contrib.providers.flagd.FlagdOptions; import dev.openfeature.contrib.providers.flagd.resolver.common.ConnectionEvent; @@ -50,380 +37,381 @@ import dev.openfeature.sdk.Value; import dev.openfeature.sdk.exceptions.ParseError; import dev.openfeature.sdk.exceptions.TypeMismatchError; +import java.lang.reflect.Field; +import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; class InProcessResolverTest { - @Test - public void connectorSetup() { - // given - FlagdOptions forGrpcOptions = FlagdOptions.builder().resolverType(Config.Resolver.IN_PROCESS) - .host("localhost") - .port(8080).build(); - FlagdOptions forOfflineOptions = FlagdOptions.builder().resolverType(Config.Resolver.IN_PROCESS) - .offlineFlagSourcePath("path").build(); - FlagdOptions forCustomConnectorOptions = FlagdOptions.builder().resolverType(Config.Resolver.IN_PROCESS) - .customConnector(new MockConnector(null)).build(); - - // then - assertInstanceOf(GrpcStreamConnector.class, InProcessResolver.getConnector(forGrpcOptions)); - assertInstanceOf(FileConnector.class, InProcessResolver.getConnector(forOfflineOptions)); - assertInstanceOf(MockConnector.class, InProcessResolver.getConnector(forCustomConnectorOptions)); - } + @Test + public void connectorSetup() { + // given + FlagdOptions forGrpcOptions = FlagdOptions.builder() + .resolverType(Config.Resolver.IN_PROCESS) + .host("localhost") + .port(8080) + .build(); + FlagdOptions forOfflineOptions = FlagdOptions.builder() + .resolverType(Config.Resolver.IN_PROCESS) + .offlineFlagSourcePath("path") + .build(); + FlagdOptions forCustomConnectorOptions = FlagdOptions.builder() + .resolverType(Config.Resolver.IN_PROCESS) + .customConnector(new MockConnector(null)) + .build(); - @Test - public void eventHandling() throws Throwable { - // given - // note - queues with adequate capacity - final BlockingQueue sender = new LinkedBlockingQueue<>(5); - final BlockingQueue receiver = new LinkedBlockingQueue<>(5); - final String key = "key1"; - final String val = "val1"; - final MutableStructure syncMetadata = new MutableStructure(); - syncMetadata.add(key, val); - - InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(new HashMap<>(), sender), - (connectionEvent) -> receiver.offer(new StorageStateChange( - connectionEvent.isConnected() ? StorageState.OK : StorageState.ERROR, - connectionEvent.getFlagsChanged(), connectionEvent.getSyncMetadata()))); - - // when - init and emit events - Thread initThread = new Thread(() -> { - try { - inProcessResolver.init(); - } catch (Exception e) { - } - }); - initThread.start(); - if (!sender.offer(new StorageStateChange(StorageState.OK, Collections.emptyList(), syncMetadata), 100, - TimeUnit.MILLISECONDS)) { - Assertions.fail("failed to send the event"); - } - if (!sender.offer(new StorageStateChange(StorageState.ERROR), 100, TimeUnit.MILLISECONDS)) { - Assertions.fail("failed to send the event"); - } - - // then - receive events in order - assertTimeoutPreemptively(Duration.ofMillis(200), () -> { - StorageStateChange storageState = receiver.take(); - assertEquals(StorageState.OK, storageState.getStorageState()); - assertEquals(val, storageState.getSyncMetadata().getValue(key).asString()); - }); - - assertTimeoutPreemptively(Duration.ofMillis(200), () -> { - assertEquals(StorageState.ERROR, receiver.take().getStorageState()); - }); - } + // then + assertInstanceOf(GrpcStreamConnector.class, InProcessResolver.getConnector(forGrpcOptions)); + assertInstanceOf(FileConnector.class, InProcessResolver.getConnector(forOfflineOptions)); + assertInstanceOf(MockConnector.class, InProcessResolver.getConnector(forCustomConnectorOptions)); + } + + @Test + void eventHandling() throws Throwable { + // given + // note - queues with adequate capacity + final BlockingQueue sender = new LinkedBlockingQueue<>(5); + final BlockingQueue receiver = new LinkedBlockingQueue<>(5); + final String key = "key1"; + final String val = "val1"; + final MutableStructure syncMetadata = new MutableStructure(); + syncMetadata.add(key, val); - @Test - public void simpleBooleanResolving() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("booleanFlag", BOOLEAN_FLAG); - - InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), - (connectionEvent) -> { - }); - - // when - ProviderEvaluation providerEvaluation = inProcessResolver.booleanEvaluation("booleanFlag", - false, - new ImmutableContext()); - - // then - assertEquals(true, providerEvaluation.getValue()); - assertEquals("on", providerEvaluation.getVariant()); - assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); + InProcessResolver inProcessResolver = getInProcessResolverWith( + new MockStorage(new HashMap<>(), sender), + connectionEvent -> receiver.offer(new StorageStateChange( + connectionEvent.isConnected() ? StorageState.OK : StorageState.ERROR, + connectionEvent.getFlagsChanged(), + connectionEvent.getSyncMetadata()))); + + // when - init and emit events + Thread initThread = new Thread(() -> { + try { + inProcessResolver.init(); + } catch (Exception e) { + } + }); + initThread.start(); + if (!sender.offer( + new StorageStateChange(StorageState.OK, Collections.emptyList(), syncMetadata), + 100, + TimeUnit.MILLISECONDS)) { + Assertions.fail("failed to send the event"); + } + if (!sender.offer(new StorageStateChange(StorageState.ERROR), 100, TimeUnit.MILLISECONDS)) { + Assertions.fail("failed to send the event"); } - @Test - public void simpleDoubleResolving() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("doubleFlag", DOUBLE_FLAG); + // then - receive events in order + assertTimeoutPreemptively(Duration.ofMillis(200), () -> { + StorageStateChange storageState = receiver.take(); + assertEquals(StorageState.OK, storageState.getStorageState()); + assertEquals(val, storageState.getSyncMetadata().getValue(key).asString()); + }); - InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), - (connectionEvent) -> { - }); + assertTimeoutPreemptively(Duration.ofMillis(200), () -> { + assertEquals(StorageState.ERROR, receiver.take().getStorageState()); + }); + } - // when - ProviderEvaluation providerEvaluation = inProcessResolver.doubleEvaluation("doubleFlag", 0d, - new ImmutableContext()); + @Test + public void simpleBooleanResolving() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("booleanFlag", BOOLEAN_FLAG); - // then - assertEquals(3.141d, providerEvaluation.getValue()); - assertEquals("one", providerEvaluation.getVariant()); - assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); - } + InProcessResolver inProcessResolver = + getInProcessResolverWith(new MockStorage(flagMap), (connectionEvent) -> {}); - @Test - public void fetchIntegerAsDouble() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("doubleFlag", DOUBLE_FLAG); + // when + ProviderEvaluation providerEvaluation = + inProcessResolver.booleanEvaluation("booleanFlag", false, new ImmutableContext()); - InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), - (connectionEvent) -> { - }); + // then + assertEquals(true, providerEvaluation.getValue()); + assertEquals("on", providerEvaluation.getVariant()); + assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); + } - // when - ProviderEvaluation providerEvaluation = inProcessResolver.integerEvaluation("doubleFlag", 0, - new ImmutableContext()); + @Test + public void simpleDoubleResolving() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("doubleFlag", DOUBLE_FLAG); - // then - assertEquals(3, providerEvaluation.getValue()); - assertEquals("one", providerEvaluation.getVariant()); - assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); - } + InProcessResolver inProcessResolver = + getInProcessResolverWith(new MockStorage(flagMap), (connectionEvent) -> {}); - @Test - public void fetchDoubleAsInt() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("integerFlag", INT_FLAG); + // when + ProviderEvaluation providerEvaluation = + inProcessResolver.doubleEvaluation("doubleFlag", 0d, new ImmutableContext()); - InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), - (connectionEvent) -> { - }); + // then + assertEquals(3.141d, providerEvaluation.getValue()); + assertEquals("one", providerEvaluation.getVariant()); + assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); + } - // when - ProviderEvaluation providerEvaluation = inProcessResolver.doubleEvaluation("integerFlag", 0d, - new ImmutableContext()); + @Test + public void fetchIntegerAsDouble() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("doubleFlag", DOUBLE_FLAG); - // then - assertEquals(1d, providerEvaluation.getValue()); - assertEquals("one", providerEvaluation.getVariant()); - assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); - } + InProcessResolver inProcessResolver = + getInProcessResolverWith(new MockStorage(flagMap), (connectionEvent) -> {}); - @Test - public void simpleIntResolving() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("integerFlag", INT_FLAG); + // when + ProviderEvaluation providerEvaluation = + inProcessResolver.integerEvaluation("doubleFlag", 0, new ImmutableContext()); - InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), - (connectionEvent) -> { - }); + // then + assertEquals(3, providerEvaluation.getValue()); + assertEquals("one", providerEvaluation.getVariant()); + assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); + } - // when - ProviderEvaluation providerEvaluation = inProcessResolver.integerEvaluation("integerFlag", 0, - new ImmutableContext()); + @Test + public void fetchDoubleAsInt() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("integerFlag", INT_FLAG); - // then - assertEquals(1, providerEvaluation.getValue()); - assertEquals("one", providerEvaluation.getVariant()); - assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); - } + InProcessResolver inProcessResolver = + getInProcessResolverWith(new MockStorage(flagMap), (connectionEvent) -> {}); - @Test - public void simpleObjectResolving() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("objectFlag", OBJECT_FLAG); + // when + ProviderEvaluation providerEvaluation = + inProcessResolver.doubleEvaluation("integerFlag", 0d, new ImmutableContext()); - InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), - (connectionEvent) -> { - }); + // then + assertEquals(1d, providerEvaluation.getValue()); + assertEquals("one", providerEvaluation.getVariant()); + assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); + } - Map typeDefault = new HashMap<>(); - typeDefault.put("key", "0164"); - typeDefault.put("date", "01.01.1990"); + @Test + public void simpleIntResolving() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("integerFlag", INT_FLAG); - // when - ProviderEvaluation providerEvaluation = inProcessResolver.objectEvaluation("objectFlag", - Value.objectToValue(typeDefault), new ImmutableContext()); + InProcessResolver inProcessResolver = + getInProcessResolverWith(new MockStorage(flagMap), (connectionEvent) -> {}); - // then - Value value = providerEvaluation.getValue(); - Map valueMap = value.asStructure().asMap(); + // when + ProviderEvaluation providerEvaluation = + inProcessResolver.integerEvaluation("integerFlag", 0, new ImmutableContext()); - assertEquals("0165", valueMap.get("key").asString()); - assertEquals("01.01.2000", valueMap.get("date").asString()); - assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); - assertEquals("typeA", providerEvaluation.getVariant()); - } + // then + assertEquals(1, providerEvaluation.getValue()); + assertEquals("one", providerEvaluation.getVariant()); + assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); + } - @Test - public void missingFlag() throws Exception { - // given - final Map flagMap = new HashMap<>(); + @Test + public void simpleObjectResolving() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("objectFlag", OBJECT_FLAG); - InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), - (connectionEvent) -> { - }); + InProcessResolver inProcessResolver = + getInProcessResolverWith(new MockStorage(flagMap), (connectionEvent) -> {}); - // when/then - ProviderEvaluation missingFlag = inProcessResolver.booleanEvaluation("missingFlag", false, - new ImmutableContext()); - assertEquals(ErrorCode.FLAG_NOT_FOUND, missingFlag.getErrorCode()); - } + Map typeDefault = new HashMap<>(); + typeDefault.put("key", "0164"); + typeDefault.put("date", "01.01.1990"); + + // when + ProviderEvaluation providerEvaluation = inProcessResolver.objectEvaluation( + "objectFlag", Value.objectToValue(typeDefault), new ImmutableContext()); - @Test - public void disabledFlag() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("disabledFlag", DISABLED_FLAG); + // then + Value value = providerEvaluation.getValue(); + Map valueMap = value.asStructure().asMap(); - InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), - (connectionEvent) -> { - }); + assertEquals("0165", valueMap.get("key").asString()); + assertEquals("01.01.2000", valueMap.get("date").asString()); + assertEquals(Reason.STATIC.toString(), providerEvaluation.getReason()); + assertEquals("typeA", providerEvaluation.getVariant()); + } - // when/then - ProviderEvaluation disabledFlag = inProcessResolver.booleanEvaluation("disabledFlag", false, - new ImmutableContext()); - assertEquals(ErrorCode.FLAG_NOT_FOUND, disabledFlag.getErrorCode()); - } + @Test + public void missingFlag() throws Exception { + // given + final Map flagMap = new HashMap<>(); - @Test - public void variantMismatchFlag() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("mismatchFlag", VARIANT_MISMATCH_FLAG); + InProcessResolver inProcessResolver = + getInProcessResolverWith(new MockStorage(flagMap), (connectionEvent) -> {}); - InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), - (connectionEvent) -> { - }); + // when/then + ProviderEvaluation missingFlag = + inProcessResolver.booleanEvaluation("missingFlag", false, new ImmutableContext()); + assertEquals(ErrorCode.FLAG_NOT_FOUND, missingFlag.getErrorCode()); + } - // when/then - assertThrows(TypeMismatchError.class, () -> { - inProcessResolver.booleanEvaluation("mismatchFlag", false, new ImmutableContext()); - }); - } + @Test + public void disabledFlag() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("disabledFlag", DISABLED_FLAG); - @Test - public void typeMismatchEvaluation() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("stringFlag", BOOLEAN_FLAG); + InProcessResolver inProcessResolver = + getInProcessResolverWith(new MockStorage(flagMap), (connectionEvent) -> {}); - InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), - (connectionEvent) -> { - }); + // when/then + ProviderEvaluation disabledFlag = + inProcessResolver.booleanEvaluation("disabledFlag", false, new ImmutableContext()); + assertEquals(ErrorCode.FLAG_NOT_FOUND, disabledFlag.getErrorCode()); + } - // when/then - assertThrows(TypeMismatchError.class, () -> { - inProcessResolver.stringEvaluation("stringFlag", "false", new ImmutableContext()); - }); - } + @Test + public void variantMismatchFlag() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("mismatchFlag", VARIANT_MISMATCH_FLAG); - @Test - public void booleanShorthandEvaluation() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("shorthand", FLAG_WIH_SHORTHAND_TARGETING); + InProcessResolver inProcessResolver = + getInProcessResolverWith(new MockStorage(flagMap), (connectionEvent) -> {}); - InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), - (connectionEvent) -> { - }); + // when/then + assertThrows(TypeMismatchError.class, () -> { + inProcessResolver.booleanEvaluation("mismatchFlag", false, new ImmutableContext()); + }); + } - ProviderEvaluation providerEvaluation = inProcessResolver.booleanEvaluation("shorthand", false, - new ImmutableContext()); + @Test + public void typeMismatchEvaluation() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("stringFlag", BOOLEAN_FLAG); - // then - assertEquals(true, providerEvaluation.getValue()); - assertEquals("true", providerEvaluation.getVariant()); - assertEquals(Reason.TARGETING_MATCH.toString(), providerEvaluation.getReason()); - } + InProcessResolver inProcessResolver = + getInProcessResolverWith(new MockStorage(flagMap), (connectionEvent) -> {}); - @Test - public void targetingMatchedEvaluationFlag() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("stringFlag", FLAG_WIH_IF_IN_TARGET); - - InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), - (connectionEvent) -> { - }); - - // when - ProviderEvaluation providerEvaluation = inProcessResolver.stringEvaluation("stringFlag", - "loopAlg", - new MutableContext().add("email", "abc@faas.com")); - - // then - assertEquals("binetAlg", providerEvaluation.getValue()); - assertEquals("binet", providerEvaluation.getVariant()); - assertEquals(Reason.TARGETING_MATCH.toString(), providerEvaluation.getReason()); - } + // when/then + assertThrows(TypeMismatchError.class, () -> { + inProcessResolver.stringEvaluation("stringFlag", "false", new ImmutableContext()); + }); + } - @Test - public void targetingUnmatchedEvaluationFlag() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("stringFlag", FLAG_WIH_IF_IN_TARGET); - - InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), - (connectionEvent) -> { - }); - - // when - ProviderEvaluation providerEvaluation = inProcessResolver.stringEvaluation("stringFlag", - "loopAlg", - new MutableContext().add("email", "abc@abc.com")); - - // then - assertEquals("loopAlg", providerEvaluation.getValue()); - assertEquals("loop", providerEvaluation.getVariant()); - assertEquals(Reason.DEFAULT.toString(), providerEvaluation.getReason()); - } + @Test + public void booleanShorthandEvaluation() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("shorthand", FLAG_WIH_SHORTHAND_TARGETING); - @Test - public void explicitTargetingKeyHandling() throws NoSuchFieldException, IllegalAccessException { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("stringFlag", FLAG_WITH_TARGETING_KEY); + InProcessResolver inProcessResolver = + getInProcessResolverWith(new MockStorage(flagMap), (connectionEvent) -> {}); - InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), - (connectionEvent) -> { - }); + ProviderEvaluation providerEvaluation = + inProcessResolver.booleanEvaluation("shorthand", false, new ImmutableContext()); - // when - ProviderEvaluation providerEvaluation = inProcessResolver.stringEvaluation("stringFlag", "loop", - new MutableContext("xyz")); + // then + assertEquals(true, providerEvaluation.getValue()); + assertEquals("true", providerEvaluation.getVariant()); + assertEquals(Reason.TARGETING_MATCH.toString(), providerEvaluation.getReason()); + } - // then - assertEquals("binetAlg", providerEvaluation.getValue()); - assertEquals("binet", providerEvaluation.getVariant()); - assertEquals(Reason.TARGETING_MATCH.toString(), providerEvaluation.getReason()); - } + @Test + public void targetingMatchedEvaluationFlag() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("stringFlag", FLAG_WIH_IF_IN_TARGET); - @Test - public void targetingErrorEvaluationFlag() throws Exception { - // given - final Map flagMap = new HashMap<>(); - flagMap.put("targetingErrorFlag", FLAG_WIH_INVALID_TARGET); + InProcessResolver inProcessResolver = + getInProcessResolverWith(new MockStorage(flagMap), (connectionEvent) -> {}); + + // when + ProviderEvaluation providerEvaluation = inProcessResolver.stringEvaluation( + "stringFlag", "loopAlg", new MutableContext().add("email", "abc@faas.com")); - InProcessResolver inProcessResolver = getInProcessResolverWith(new MockStorage(flagMap), - (connectionEvent) -> { - }); + // then + assertEquals("binetAlg", providerEvaluation.getValue()); + assertEquals("binet", providerEvaluation.getVariant()); + assertEquals(Reason.TARGETING_MATCH.toString(), providerEvaluation.getReason()); + } - // when/then - assertThrows(ParseError.class, () -> { - inProcessResolver.booleanEvaluation("targetingErrorFlag", false, new ImmutableContext()); - }); - } + @Test + public void targetingUnmatchedEvaluationFlag() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("stringFlag", FLAG_WIH_IF_IN_TARGET); - @Test - public void validateMetadataInEvaluationResult() throws Exception { - // given - final String scope = "appName=myApp"; - final Map flagMap = new HashMap<>(); - flagMap.put("booleanFlag", BOOLEAN_FLAG); - - InProcessResolver inProcessResolver = getInProcessResolverWith( - FlagdOptions.builder().selector(scope).build(), - new MockStorage(flagMap)); - - // when - ProviderEvaluation providerEvaluation = inProcessResolver.booleanEvaluation("booleanFlag", - false, - new ImmutableContext()); - - // then - final ImmutableMetadata metadata = providerEvaluation.getFlagMetadata(); - assertNotNull(metadata); - assertEquals(scope, metadata.getString("scope")); - } + InProcessResolver inProcessResolver = + getInProcessResolverWith(new MockStorage(flagMap), (connectionEvent) -> {}); + + // when + ProviderEvaluation providerEvaluation = inProcessResolver.stringEvaluation( + "stringFlag", "loopAlg", new MutableContext().add("email", "abc@abc.com")); + + // then + assertEquals("loopAlg", providerEvaluation.getValue()); + assertEquals("loop", providerEvaluation.getVariant()); + assertEquals(Reason.DEFAULT.toString(), providerEvaluation.getReason()); + } + + @Test + public void explicitTargetingKeyHandling() throws NoSuchFieldException, IllegalAccessException { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("stringFlag", FLAG_WITH_TARGETING_KEY); + + InProcessResolver inProcessResolver = + getInProcessResolverWith(new MockStorage(flagMap), (connectionEvent) -> {}); + + // when + ProviderEvaluation providerEvaluation = + inProcessResolver.stringEvaluation("stringFlag", "loop", new MutableContext("xyz")); + + // then + assertEquals("binetAlg", providerEvaluation.getValue()); + assertEquals("binet", providerEvaluation.getVariant()); + assertEquals(Reason.TARGETING_MATCH.toString(), providerEvaluation.getReason()); + } + + @Test + public void targetingErrorEvaluationFlag() throws Exception { + // given + final Map flagMap = new HashMap<>(); + flagMap.put("targetingErrorFlag", FLAG_WIH_INVALID_TARGET); + + InProcessResolver inProcessResolver = + getInProcessResolverWith(new MockStorage(flagMap), (connectionEvent) -> {}); + + // when/then + assertThrows(ParseError.class, () -> { + inProcessResolver.booleanEvaluation("targetingErrorFlag", false, new ImmutableContext()); + }); + } + + @Test + public void validateMetadataInEvaluationResult() throws Exception { + // given + final String scope = "appName=myApp"; + final Map flagMap = new HashMap<>(); + flagMap.put("booleanFlag", BOOLEAN_FLAG); + + InProcessResolver inProcessResolver = + getInProcessResolverWith(FlagdOptions.builder().selector(scope).build(), new MockStorage(flagMap)); + + // when + ProviderEvaluation providerEvaluation = + inProcessResolver.booleanEvaluation("booleanFlag", false, new ImmutableContext()); + + // then + final ImmutableMetadata metadata = providerEvaluation.getFlagMetadata(); + assertNotNull(metadata); + assertEquals(scope, metadata.getString("scope")); + } @Test void selectorIsAddedToFlagMetadata() throws Exception { @@ -431,18 +419,12 @@ void selectorIsAddedToFlagMetadata() throws Exception { final Map flagMap = new HashMap<>(); flagMap.put("flag", INT_FLAG); - InProcessResolver inProcessResolver = getInProcessResolverWith( - new MockStorage(flagMap), - connectionEvent -> { - }, - "selector"); + InProcessResolver inProcessResolver = + getInProcessResolverWith(new MockStorage(flagMap), connectionEvent -> {}, "selector"); // when - ProviderEvaluation providerEvaluation = inProcessResolver.integerEvaluation( - "flag", - 0, - new ImmutableContext() - ); + ProviderEvaluation providerEvaluation = + inProcessResolver.integerEvaluation("flag", 0, new ImmutableContext()); // then assertThat(providerEvaluation.getFlagMetadata()).isNotNull(); @@ -455,29 +437,14 @@ void selectorIsOverwrittenByFlagMetadata() throws Exception { final Map flagMap = new HashMap<>(); final Map flagMetadata = new HashMap<>(); flagMetadata.put("scope", "new selector"); - flagMap.put( - "flag", - new FeatureFlag( - "stage", - "loop", - stringVariants, - "", - flagMetadata - ) - ); + flagMap.put("flag", new FeatureFlag("stage", "loop", stringVariants, "", flagMetadata)); - InProcessResolver inProcessResolver = getInProcessResolverWith( - new MockStorage(flagMap), - connectionEvent -> { - }, - "selector"); + InProcessResolver inProcessResolver = + getInProcessResolverWith(new MockStorage(flagMap), connectionEvent -> {}, "selector"); // when - ProviderEvaluation providerEvaluation = inProcessResolver.stringEvaluation( - "flag", - "def", - new ImmutableContext() - ); + ProviderEvaluation providerEvaluation = + inProcessResolver.stringEvaluation("flag", "def", new ImmutableContext()); // then assertThat(providerEvaluation.getFlagMetadata()).isNotNull(); @@ -487,27 +454,21 @@ void selectorIsOverwrittenByFlagMetadata() throws Exception { private InProcessResolver getInProcessResolverWith(final FlagdOptions options, final MockStorage storage) throws NoSuchFieldException, IllegalAccessException { - final InProcessResolver resolver = new InProcessResolver( - options, - () -> true, - connectionEvent -> { - } - ); + final InProcessResolver resolver = new InProcessResolver(options, () -> true, connectionEvent -> {}); return injectFlagStore(resolver, storage); } - private InProcessResolver getInProcessResolverWith(final MockStorage storage, - final Consumer onConnectionEvent) + private InProcessResolver getInProcessResolverWith( + final MockStorage storage, final Consumer onConnectionEvent) throws NoSuchFieldException, IllegalAccessException { - final InProcessResolver resolver = new InProcessResolver( - FlagdOptions.builder().deadline(1000).build(), () -> true, onConnectionEvent); + final InProcessResolver resolver = + new InProcessResolver(FlagdOptions.builder().deadline(1000).build(), () -> true, onConnectionEvent); return injectFlagStore(resolver, storage); } - private InProcessResolver getInProcessResolverWith(final MockStorage storage, - final Consumer onConnectionEvent, - String selector) + private InProcessResolver getInProcessResolverWith( + final MockStorage storage, final Consumer onConnectionEvent, String selector) throws NoSuchFieldException, IllegalAccessException { final InProcessResolver resolver = new InProcessResolver( @@ -515,15 +476,14 @@ private InProcessResolver getInProcessResolverWith(final MockStorage storage, return injectFlagStore(resolver, storage); } - // helper to inject flagStore override - private InProcessResolver injectFlagStore(final InProcessResolver resolver, final MockStorage storage) - throws NoSuchFieldException, IllegalAccessException { + // helper to inject flagStore override + private InProcessResolver injectFlagStore(final InProcessResolver resolver, final MockStorage storage) + throws NoSuchFieldException, IllegalAccessException { - final Field flagStore = InProcessResolver.class.getDeclaredField("flagStore"); - flagStore.setAccessible(true); - flagStore.set(resolver, storage); - - return resolver; - } + final Field flagStore = InProcessResolver.class.getDeclaredField("flagStore"); + flagStore.setAccessible(true); + flagStore.set(resolver, storage); + return resolver; + } } diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/MockFlags.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/MockFlags.java index e81b1fb43..6e1a94987 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/MockFlags.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/MockFlags.java @@ -51,7 +51,8 @@ public class MockFlags { static final FeatureFlag BOOLEAN_FLAG = new FeatureFlag("ENABLED", "on", booleanVariant, null, new HashMap<>()); // correct flag - boolean - static final FeatureFlag SHORTHAND_FLAG = new FeatureFlag("ENABLED", "false", booleanVariant, null, new HashMap<>()); + static final FeatureFlag SHORTHAND_FLAG = + new FeatureFlag("ENABLED", "false", booleanVariant, null, new HashMap<>()); // correct flag - double static final FeatureFlag DOUBLE_FLAG = new FeatureFlag("ENABLED", "one", doubleVariants, null, new HashMap<>()); @@ -66,20 +67,29 @@ public class MockFlags { static final FeatureFlag DISABLED_FLAG = new FeatureFlag("DISABLED", "on", booleanVariant, null, new HashMap<>()); // incorrect flag - variant mismatch - static final FeatureFlag VARIANT_MISMATCH_FLAG = new FeatureFlag("ENABLED", "true", stringVariants, null, new HashMap<>()); + static final FeatureFlag VARIANT_MISMATCH_FLAG = + new FeatureFlag("ENABLED", "true", stringVariants, null, new HashMap<>()); // flag with targeting rule - string - static final FeatureFlag FLAG_WIH_IF_IN_TARGET = new FeatureFlag("ENABLED", "loop", stringVariants, - "{\"if\":[{\"in\":[\"@faas.com\",{\"var\":[\"email\"]}]},\"binet\",null]}", new HashMap<>()); - - static final FeatureFlag FLAG_WITH_TARGETING_KEY = new FeatureFlag("ENABLED", "loop", stringVariants, - "{\"if\":[{\"==\":[{\"var\":\"targetingKey\"},\"xyz\"]},\"binet\",null]}", new HashMap<>()); + static final FeatureFlag FLAG_WIH_IF_IN_TARGET = new FeatureFlag( + "ENABLED", + "loop", + stringVariants, + "{\"if\":[{\"in\":[\"@faas.com\",{\"var\":[\"email\"]}]},\"binet\",null]}", + new HashMap<>()); + + static final FeatureFlag FLAG_WITH_TARGETING_KEY = new FeatureFlag( + "ENABLED", + "loop", + stringVariants, + "{\"if\":[{\"==\":[{\"var\":\"targetingKey\"},\"xyz\"]},\"binet\",null]}", + new HashMap<>()); // flag with incorrect targeting rule - static final FeatureFlag FLAG_WIH_INVALID_TARGET = new FeatureFlag("ENABLED", "loop", stringVariants, - "{if this, then that}", new HashMap<>()); + static final FeatureFlag FLAG_WIH_INVALID_TARGET = + new FeatureFlag("ENABLED", "loop", stringVariants, "{if this, then that}", new HashMap<>()); // flag with shorthand rule - static final FeatureFlag FLAG_WIH_SHORTHAND_TARGETING = new FeatureFlag("ENABLED", "false", shorthandVariant, - "{ \"if\": [true, true, false] }", new HashMap<>()); + static final FeatureFlag FLAG_WIH_SHORTHAND_TARGETING = + new FeatureFlag("ENABLED", "false", shorthandVariant, "{ \"if\": [true, true, false] }", new HashMap<>()); } diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/TestUtils.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/TestUtils.java index a603ea266..e042888be 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/TestUtils.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/TestUtils.java @@ -15,6 +15,8 @@ public class TestUtils { public static final String VALID_LONG = "flagConfigurations/valid-long.json"; public static final String INVALID_FLAG = "flagConfigurations/invalid-flag.json"; public static final String INVALID_FLAG_METADATA = "flagConfigurations/invalid-metadata.json"; + public static final String INVALID_GLOBAL_FLAG_METADATA = "flagConfigurations/invalid-global-metadata.json"; + public static final String VALID_GLOBAL_FLAG_METADATA = "flagConfigurations/valid-global-metadata.json"; public static final String INVALID_CFG = "flagConfigurations/invalid-configuration.json"; public static final String UPDATABLE_FILE = "flagConfigurations/updatableFlags.json"; diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParserTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParserTest.java index 8431e5b6e..a97d24545 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParserTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParserTest.java @@ -3,6 +3,8 @@ import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.INVALID_CFG; import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.INVALID_FLAG; import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.INVALID_FLAG_METADATA; +import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.INVALID_GLOBAL_FLAG_METADATA; +import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.VALID_GLOBAL_FLAG_METADATA; import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.VALID_LONG; import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.VALID_SIMPLE; import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.VALID_SIMPLE_EXTRA_FIELD; @@ -41,6 +43,12 @@ void validJsonConfigurationParsing() throws IOException { assertInstanceOf(Double.class, metadata.get("float")); assertEquals(1.234, metadata.get("float")); + + assertNotNull(boolFlag.getMetadata()); + assertEquals(3, boolFlag.getMetadata().size()); + assertEquals("string", boolFlag.getMetadata().get("string")); + assertEquals(true, boolFlag.getMetadata().get("boolean")); + assertEquals(1.234, boolFlag.getMetadata().get("float")); } @Test @@ -78,27 +86,59 @@ void validJsonConfigurationWithTargetingRulesParsing() throws IOException { "{\"if\":[{\"in\":[\"@faas.com\",{\"var\":[\"email\"]}]},\"binet\",null]}", stringFlag.getTargeting()); } + @Test + void validJsonConfigurationWithGlobalMetadataParsing() throws IOException { + Map flagMap = + FlagParser.parseString(getFlagsFromResource(VALID_GLOBAL_FLAG_METADATA), true); + FeatureFlag flag = flagMap.get("without-metadata"); + + assertNotNull(flag); + + Map metadata = flag.getMetadata(); + + assertNotNull(metadata); + assertEquals("some string", metadata.get("string")); + assertEquals(true, metadata.get("boolean")); + assertEquals(1.234, metadata.get("float")); + } + + @Test + void validJsonConfigurationWithGlobalMetadataGetsOverwrittenParsing() throws IOException { + Map flagMap = + FlagParser.parseString(getFlagsFromResource(VALID_GLOBAL_FLAG_METADATA), true); + FeatureFlag flag = flagMap.get("with-metadata"); + + assertNotNull(flag); + + Map metadata = flag.getMetadata(); + + assertNotNull(metadata); + assertEquals("other string", metadata.get("string")); + assertEquals(true, metadata.get("boolean")); + assertEquals(2.71828, metadata.get("float")); + } + @Test void invalidFlagThrowsError() throws IOException { String flagString = getFlagsFromResource(INVALID_FLAG); - assertThrows(IllegalArgumentException.class, () -> { - FlagParser.parseString(flagString, true); - }); + assertThrows(IllegalArgumentException.class, () -> FlagParser.parseString(flagString, true)); } @Test void invalidFlagMetadataThrowsError() throws IOException { String flagString = getFlagsFromResource(INVALID_FLAG_METADATA); - assertThrows(IllegalArgumentException.class, () -> { - FlagParser.parseString(flagString, true); - }); + assertThrows(IllegalArgumentException.class, () -> FlagParser.parseString(flagString, true)); + } + + @Test + void invalidGlobalFlagMetadataThrowsError() throws IOException { + String flagString = getFlagsFromResource(INVALID_GLOBAL_FLAG_METADATA); + assertThrows(IllegalArgumentException.class, () -> FlagParser.parseString(flagString, true)); } @Test void invalidConfigurationsThrowsError() throws IOException { String flagString = getFlagsFromResource(INVALID_CFG); - assertThrows(IllegalArgumentException.class, () -> { - FlagParser.parseString(flagString, true); - }); + assertThrows(IllegalArgumentException.class, () -> FlagParser.parseString(flagString, true)); } } diff --git a/providers/flagd/src/test/resources/flagConfigurations/invalid-global-metadata.json b/providers/flagd/src/test/resources/flagConfigurations/invalid-global-metadata.json new file mode 100644 index 000000000..9caf8200d --- /dev/null +++ b/providers/flagd/src/test/resources/flagConfigurations/invalid-global-metadata.json @@ -0,0 +1,21 @@ +{ + "$schema": "../../../main/resources/flagd/schemas/flags.json", + "metadata": { + "string": "string", + "boolean": true, + "float": 1.234, + "invalid": { + "a": "a" + } + }, + "flags": { + "myBoolFlag": { + "state": "ENABLED", + "variants": { + "on": true, + "off": false + }, + "defaultVariant": "on" + } + } +} diff --git a/providers/flagd/src/test/resources/flagConfigurations/valid-global-metadata.json b/providers/flagd/src/test/resources/flagConfigurations/valid-global-metadata.json new file mode 100644 index 000000000..f5323d9c4 --- /dev/null +++ b/providers/flagd/src/test/resources/flagConfigurations/valid-global-metadata.json @@ -0,0 +1,31 @@ +{ + "$schema": "../../../main/resources/flagd/schemas/flags.json", + "metadata": { + "string": "some string", + "boolean": true, + "float": 1.234 + }, + "flags": { + "without-metadata": { + "state": "ENABLED", + "variants": { + "on": true, + "off": false + }, + "defaultVariant": "on" + }, + "with-metadata": { + "state": "ENABLED", + "variants": { + "on": true, + "off": false + }, + "defaultVariant": "on", + "metadata": { + "string": "other string", + "boolean": true, + "float": 2.71828 + } + } + } +} From 18219c0f79ff2c89f0de516785bd4b46bddaed56 Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Thu, 9 Jan 2025 10:49:32 +0100 Subject: [PATCH 05/12] fixup! feat: Update in-process resolver to support flag metadata #1102 Signed-off-by: christian.lutnik --- .../flagd/resolver/process/MockFlags.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/MockFlags.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/MockFlags.java index 6e1a94987..5cd179e02 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/MockFlags.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/MockFlags.java @@ -48,27 +48,27 @@ public class MockFlags { } // correct flag - boolean - static final FeatureFlag BOOLEAN_FLAG = new FeatureFlag("ENABLED", "on", booleanVariant, null, new HashMap<>()); + static final FeatureFlag BOOLEAN_FLAG = new FeatureFlag("ENABLED", "on", booleanVariant, null); // correct flag - boolean static final FeatureFlag SHORTHAND_FLAG = - new FeatureFlag("ENABLED", "false", booleanVariant, null, new HashMap<>()); + new FeatureFlag("ENABLED", "false", booleanVariant, null); // correct flag - double - static final FeatureFlag DOUBLE_FLAG = new FeatureFlag("ENABLED", "one", doubleVariants, null, new HashMap<>()); + static final FeatureFlag DOUBLE_FLAG = new FeatureFlag("ENABLED", "one", doubleVariants, null); // correct flag - int - static final FeatureFlag INT_FLAG = new FeatureFlag("ENABLED", "one", intVariants, null, new HashMap<>()); + static final FeatureFlag INT_FLAG = new FeatureFlag("ENABLED", "one", intVariants, null); // correct flag - object - static final FeatureFlag OBJECT_FLAG = new FeatureFlag("ENABLED", "typeA", objectVariants, null, new HashMap<>()); + static final FeatureFlag OBJECT_FLAG = new FeatureFlag("ENABLED", "typeA", objectVariants, null); // flag in disabled state - static final FeatureFlag DISABLED_FLAG = new FeatureFlag("DISABLED", "on", booleanVariant, null, new HashMap<>()); + static final FeatureFlag DISABLED_FLAG = new FeatureFlag("DISABLED", "on", booleanVariant, null); // incorrect flag - variant mismatch static final FeatureFlag VARIANT_MISMATCH_FLAG = - new FeatureFlag("ENABLED", "true", stringVariants, null, new HashMap<>()); + new FeatureFlag("ENABLED", "true", stringVariants, null); // flag with targeting rule - string static final FeatureFlag FLAG_WIH_IF_IN_TARGET = new FeatureFlag( @@ -87,9 +87,9 @@ public class MockFlags { // flag with incorrect targeting rule static final FeatureFlag FLAG_WIH_INVALID_TARGET = - new FeatureFlag("ENABLED", "loop", stringVariants, "{if this, then that}", new HashMap<>()); + new FeatureFlag("ENABLED", "loop", stringVariants, "{if this, then that}"); // flag with shorthand rule static final FeatureFlag FLAG_WIH_SHORTHAND_TARGETING = - new FeatureFlag("ENABLED", "false", shorthandVariant, "{ \"if\": [true, true, false] }", new HashMap<>()); + new FeatureFlag("ENABLED", "false", shorthandVariant, "{ \"if\": [true, true, false] }"); } From af0006c6595d5e5f2943e0872d5b90efc4b6b5df Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Thu, 9 Jan 2025 11:28:18 +0100 Subject: [PATCH 06/12] fixup! feat: Update in-process resolver to support flag metadata #1102 Signed-off-by: christian.lutnik --- .../flagd/resolver/process/InProcessResolver.java | 3 ++- .../providers/flagd/resolver/process/MockFlags.java | 12 ++++-------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java index 163d288c0..4f46f36cd 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java @@ -64,7 +64,8 @@ public InProcessResolver( this.fallBackMetadata = null; } else { this.scope = options.getSelector(); - this.fallBackMetadata = ImmutableMetadata.builder().addString("scope", this.scope).build(); + this.fallBackMetadata = + ImmutableMetadata.builder().addString("scope", this.scope).build(); } } diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/MockFlags.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/MockFlags.java index 5cd179e02..fef135cc9 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/MockFlags.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/MockFlags.java @@ -51,8 +51,7 @@ public class MockFlags { static final FeatureFlag BOOLEAN_FLAG = new FeatureFlag("ENABLED", "on", booleanVariant, null); // correct flag - boolean - static final FeatureFlag SHORTHAND_FLAG = - new FeatureFlag("ENABLED", "false", booleanVariant, null); + static final FeatureFlag SHORTHAND_FLAG = new FeatureFlag("ENABLED", "false", booleanVariant, null); // correct flag - double static final FeatureFlag DOUBLE_FLAG = new FeatureFlag("ENABLED", "one", doubleVariants, null); @@ -67,23 +66,20 @@ public class MockFlags { static final FeatureFlag DISABLED_FLAG = new FeatureFlag("DISABLED", "on", booleanVariant, null); // incorrect flag - variant mismatch - static final FeatureFlag VARIANT_MISMATCH_FLAG = - new FeatureFlag("ENABLED", "true", stringVariants, null); + static final FeatureFlag VARIANT_MISMATCH_FLAG = new FeatureFlag("ENABLED", "true", stringVariants, null); // flag with targeting rule - string static final FeatureFlag FLAG_WIH_IF_IN_TARGET = new FeatureFlag( "ENABLED", "loop", stringVariants, - "{\"if\":[{\"in\":[\"@faas.com\",{\"var\":[\"email\"]}]},\"binet\",null]}", - new HashMap<>()); + "{\"if\":[{\"in\":[\"@faas.com\",{\"var\":[\"email\"]}]},\"binet\",null]}"); static final FeatureFlag FLAG_WITH_TARGETING_KEY = new FeatureFlag( "ENABLED", "loop", stringVariants, - "{\"if\":[{\"==\":[{\"var\":\"targetingKey\"},\"xyz\"]},\"binet\",null]}", - new HashMap<>()); + "{\"if\":[{\"==\":[{\"var\":\"targetingKey\"},\"xyz\"]},\"binet\",null]}"); // flag with incorrect targeting rule static final FeatureFlag FLAG_WIH_INVALID_TARGET = From f8e69aa5e071e378fe7ab2eee88cf071a4f6066d Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Thu, 9 Jan 2025 15:23:06 +0100 Subject: [PATCH 07/12] fixup! feat: Update in-process resolver to support flag metadata #1102 Signed-off-by: christian.lutnik --- providers/flagd/schemas | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/flagd/schemas b/providers/flagd/schemas index 58aeed308..37baa2cde 160000 --- a/providers/flagd/schemas +++ b/providers/flagd/schemas @@ -1 +1 @@ -Subproject commit 58aeed308d1a851ce47f17266128b99b28761af6 +Subproject commit 37baa2cdea48a5ac614ba3e718b7d02ad4120611 From 65a508c2b6017b65edcfb64cac20c5055f291563 Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Fri, 10 Jan 2025 10:44:26 +0100 Subject: [PATCH 08/12] fixup! feat: Update in-process resolver to support flag metadata #1102 Signed-off-by: christian.lutnik --- .../resolver/process/InProcessResolver.java | 76 +++++++++++-------- .../resolver/process/model/FeatureFlag.java | 12 --- .../resolver/process/model/FlagParser.java | 13 ++-- .../resolver/process/model/ParsingResult.java | 22 ++++++ .../resolver/process/storage/FlagStore.java | 24 ++++-- .../resolver/process/storage/Storage.java | 3 +- .../process/storage/StorageQueryResult.java | 24 ++++++ .../process/InProcessResolverTest.java | 65 ++++++++++++++++ .../flagd/resolver/process/MockStorage.java | 16 +++- .../process/model/FlagParserTest.java | 36 ++++++--- .../process/storage/FlagStoreTest.java | 7 +- 11 files changed, 222 insertions(+), 76 deletions(-) create mode 100644 providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/ParsingResult.java create mode 100644 providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/StorageQueryResult.java diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java index 4f46f36cd..86611a9f6 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java @@ -9,6 +9,7 @@ import dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag; import dev.openfeature.contrib.providers.flagd.resolver.process.storage.FlagStore; import dev.openfeature.contrib.providers.flagd.resolver.process.storage.Storage; +import dev.openfeature.contrib.providers.flagd.resolver.process.storage.StorageQueryResult; import dev.openfeature.contrib.providers.flagd.resolver.process.storage.StorageStateChange; import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.Connector; import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.file.FileConnector; @@ -46,7 +47,7 @@ public class InProcessResolver implements Resolver { * Resolves flag values using https://buf.build/open-feature/flagd/docs/main:flagd.sync.v1. Flags * are evaluated locally. * - * @param options flagd options + * @param options flagd options * @param connectedSupplier lambda providing current connection status from caller * @param onConnectionEvent lambda which handles changes in the connection/stream */ @@ -161,14 +162,15 @@ static Connector getConnector(final FlagdOptions options) { } private ProviderEvaluation resolve(Class type, String key, EvaluationContext ctx) { - final FeatureFlag flag = flagStore.getFlag(key); + final StorageQueryResult storageQueryResult = flagStore.getFlag(key); + final FeatureFlag flag = storageQueryResult.getFeatureFlag(); // missing flag if (flag == null) { return ProviderEvaluation.builder() .errorMessage("flag: " + key + " not found") .errorCode(ErrorCode.FLAG_NOT_FOUND) - .flagMetadata(fallBackMetadata) + .flagMetadata(getFlagMetadata(storageQueryResult)) .build(); } @@ -177,7 +179,7 @@ private ProviderEvaluation resolve(Class type, String key, EvaluationC return ProviderEvaluation.builder() .errorMessage("flag: " + key + " is disabled") .errorCode(ErrorCode.FLAG_NOT_FOUND) - .flagMetadata(getFlagMetadata(flag)) + .flagMetadata(getFlagMetadata(storageQueryResult)) .build(); } @@ -228,47 +230,55 @@ private ProviderEvaluation resolve(Class type, String key, EvaluationC .value((T) value) .variant(resolvedVariant) .reason(reason) - .flagMetadata(getFlagMetadata(flag)) + .flagMetadata(getFlagMetadata(storageQueryResult)) .build(); } - private ImmutableMetadata getFlagMetadata(FeatureFlag flag) { - if (flag == null) { - return fallBackMetadata; + private ImmutableMetadata getFlagMetadata(StorageQueryResult storageQueryResult) { + ImmutableMetadata.ImmutableMetadataBuilder metadataBuilder = ImmutableMetadata.builder(); + for (Map.Entry entry : + storageQueryResult.getGlobalFlagMetadata().entrySet()) { + addEntryToMetadataBuilder(metadataBuilder, entry.getKey(), entry.getValue()); } - ImmutableMetadata.ImmutableMetadataBuilder metadataBuilder = ImmutableMetadata.builder(); if (scope != null) { metadataBuilder.addString("scope", scope); } - for (Map.Entry entry : flag.getMetadata().entrySet()) { - Object value = entry.getValue(); - if (value instanceof Number) { - if (value instanceof Long) { - metadataBuilder.addLong(entry.getKey(), (Long) value); - continue; - } else if (value instanceof Double) { - metadataBuilder.addDouble(entry.getKey(), (Double) value); - continue; - } else if (value instanceof Integer) { - metadataBuilder.addInteger(entry.getKey(), (Integer) value); - continue; - } else if (value instanceof Float) { - metadataBuilder.addFloat(entry.getKey(), (Float) value); - continue; - } - } else if (value instanceof Boolean) { - metadataBuilder.addBoolean(entry.getKey(), (Boolean) value); - continue; - } else if (value instanceof String) { - metadataBuilder.addString(entry.getKey(), (String) value); - continue; + FeatureFlag flag = storageQueryResult.getFeatureFlag(); + if (flag != null) { + for (Map.Entry entry : flag.getMetadata().entrySet()) { + addEntryToMetadataBuilder(metadataBuilder, entry.getKey(), entry.getValue()); } - throw new IllegalArgumentException("The type of the Metadata entry with key " + entry.getKey() - + " and value " + entry.getValue() + " is not supported"); } return metadataBuilder.build(); } + + private void addEntryToMetadataBuilder( + ImmutableMetadata.ImmutableMetadataBuilder metadataBuilder, String key, Object value) { + if (value instanceof Number) { + if (value instanceof Long) { + metadataBuilder.addLong(key, (Long) value); + return; + } else if (value instanceof Double) { + metadataBuilder.addDouble(key, (Double) value); + return; + } else if (value instanceof Integer) { + metadataBuilder.addInteger(key, (Integer) value); + return; + } else if (value instanceof Float) { + metadataBuilder.addFloat(key, (Float) value); + return; + } + } else if (value instanceof Boolean) { + metadataBuilder.addBoolean(key, (Boolean) value); + return; + } else if (value instanceof String) { + metadataBuilder.addString(key, (String) value); + return; + } + throw new IllegalArgumentException( + "The type of the Metadata entry with key " + key + " and value " + value + " is not supported"); + } } diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FeatureFlag.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FeatureFlag.java index 95d46a102..2eaf2ed87 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FeatureFlag.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FeatureFlag.java @@ -54,18 +54,6 @@ public FeatureFlag(String state, String defaultVariant, Map vari this.metadata = new HashMap<>(); } - /** - * Add global metadata to this FeatureFlag. Keys that already exist in the metadata of this flag are not - * overwritten. - * - * @param metadata The metadata to add to this flag - */ - public void addMetadata(Map metadata) { - for (Map.Entry entry : metadata.entrySet()) { - this.metadata.putIfAbsent(entry.getKey(), entry.getValue()); - } - } - /** Get targeting rule of the flag. */ public String getTargeting() { return this.targeting == null ? EMPTY_TARGETING_STRING : this.targeting; diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParser.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParser.java index 1dbbc3030..7b6a5d4b2 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParser.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParser.java @@ -53,8 +53,7 @@ private FlagParser() {} } /** Parse {@link String} for feature flags. */ - public static Map parseString(final String configuration, boolean throwIfInvalid) - throws IOException { + public static ParsingResult parseString(final String configuration, boolean throwIfInvalid) throws IOException { if (SCHEMA_VALIDATOR != null) { try (JsonParser parser = MAPPER.createParser(configuration)) { Set validationMessages = SCHEMA_VALIDATOR.validate(parser.readValueAsTree()); @@ -72,12 +71,12 @@ public static Map parseString(final String configuration, b final String transposedConfiguration = transposeEvaluators(configuration); final Map flagMap = new HashMap<>(); - + final Map metadata; try (JsonParser parser = MAPPER.createParser(transposedConfiguration)) { final TreeNode treeNode = parser.readValueAsTree(); final TreeNode flagNode = treeNode.get(FLAG_KEY); final TreeNode metadataNode = treeNode.get(METADATA_KEY); - final Map metadata = parseMetadata(metadataNode); + metadata = parseMetadata(metadataNode); if (flagNode == null) { throw new IllegalArgumentException("No flag configurations found in the payload"); @@ -86,13 +85,11 @@ public static Map parseString(final String configuration, b final Iterator it = flagNode.fieldNames(); while (it.hasNext()) { final String key = it.next(); - FeatureFlag flag = MAPPER.treeToValue(flagNode.get(key), FeatureFlag.class); - flag.addMetadata(metadata); - flagMap.put(key, flag); + flagMap.put(key, MAPPER.treeToValue(flagNode.get(key), FeatureFlag.class)); } } - return flagMap; + return new ParsingResult(flagMap, metadata); } private static Map parseMetadata(TreeNode metadataNode) throws JsonProcessingException { diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/ParsingResult.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/ParsingResult.java new file mode 100644 index 000000000..c368380e2 --- /dev/null +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/ParsingResult.java @@ -0,0 +1,22 @@ +package dev.openfeature.contrib.providers.flagd.resolver.process.model; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.util.Map; +import lombok.Getter; + +/** + * The result of the parsing of a json string containing feature flag definitions. + */ +@Getter +@SuppressFBWarnings( + value = {"EI_EXPOSE_REP"}, + justification = "Feature flag comes as a Json configuration, hence they must be exposed") +public class ParsingResult { + private final Map flags; + private final Map globalFlagMetadata; + + public ParsingResult(Map flags, Map globalFlagMetadata) { + this.flags = flags; + this.globalFlagMetadata = globalFlagMetadata; + } +} diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/FlagStore.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/FlagStore.java index 7d419a353..1b6bd5f47 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/FlagStore.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/FlagStore.java @@ -4,6 +4,7 @@ import dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag; import dev.openfeature.contrib.providers.flagd.resolver.process.model.FlagParser; +import dev.openfeature.contrib.providers.flagd.resolver.process.model.ParsingResult; import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.Connector; import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayload; import dev.openfeature.flagd.grpc.sync.Sync.GetMetadataResponse; @@ -35,6 +36,7 @@ public class FlagStore implements Storage { private final AtomicBoolean shutdown = new AtomicBoolean(false); private final BlockingQueue stateBlockingQueue = new LinkedBlockingQueue<>(1); private final Map flags = new HashMap<>(); + private final Map globalFlagMetadata = new HashMap<>(); private final Connector connector; private final boolean throwIfInvalid; @@ -49,6 +51,7 @@ public FlagStore(final Connector connector, final boolean throwIfInvalid) { } /** Initialize storage layer. */ + @Override public void init() throws Exception { connector.init(); Thread streamer = new Thread(() -> { @@ -68,6 +71,7 @@ public void init() throws Exception { * * @throws InterruptedException if stream can't be closed within deadline. */ + @Override public void shutdown() throws InterruptedException { if (shutdown.getAndSet(true)) { return; @@ -76,17 +80,23 @@ public void shutdown() throws InterruptedException { connector.shutdown(); } - /** Retrieve flag for the given key. */ - public FeatureFlag getFlag(final String key) { + /** Retrieve flag for the given key and the global flag metadata. */ + @Override + public StorageQueryResult getFlag(final String key) { readLock.lock(); + FeatureFlag flag; + Map metadata; try { - return flags.get(key); + flag = flags.get(key); + metadata = new HashMap<>(globalFlagMetadata); } finally { readLock.unlock(); } + return new StorageQueryResult(flag, metadata); } /** Retrieve blocking queue to check storage status. */ + @Override public BlockingQueue getStateQueue() { return stateBlockingQueue; } @@ -100,14 +110,18 @@ private void streamerListener(final Connector connector) throws InterruptedExcep case DATA: try { List changedFlagsKeys; - Map flagMap = - FlagParser.parseString(payload.getFlagData(), throwIfInvalid); + ParsingResult parsingResult = FlagParser.parseString(payload.getFlagData(), throwIfInvalid); + Map flagMap = parsingResult.getFlags(); + Map globalFlagMetadataMap = parsingResult.getGlobalFlagMetadata(); + Structure metadata = parseSyncMetadata(payload.getMetadataResponse()); writeLock.lock(); try { changedFlagsKeys = getChangedFlagsKeys(flagMap); flags.clear(); flags.putAll(flagMap); + globalFlagMetadata.clear(); + globalFlagMetadata.putAll(globalFlagMetadataMap); } finally { writeLock.unlock(); } diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/Storage.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/Storage.java index c18ce82b2..38cd89e26 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/Storage.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/Storage.java @@ -1,6 +1,5 @@ package dev.openfeature.contrib.providers.flagd.resolver.process.storage; -import dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag; import java.util.concurrent.BlockingQueue; /** Storage abstraction for resolver. */ @@ -9,7 +8,7 @@ public interface Storage { void shutdown() throws InterruptedException; - FeatureFlag getFlag(final String key); + StorageQueryResult getFlag(final String key); BlockingQueue getStateQueue(); } diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/StorageQueryResult.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/StorageQueryResult.java new file mode 100644 index 000000000..7474ab873 --- /dev/null +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/StorageQueryResult.java @@ -0,0 +1,24 @@ +package dev.openfeature.contrib.providers.flagd.resolver.process.storage; + +import dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.util.Map; +import lombok.Getter; + +/** + * To be returned by the storage when a flag is queried. Contains the flag (iff a flag associated with the given key + * exists, null otherwise) and global flag metadata + */ +@Getter +@SuppressFBWarnings( + value = {"EI_EXPOSE_REP"}, + justification = "The storage provides access to both feature flags and global metadata") +public class StorageQueryResult { + private final FeatureFlag featureFlag; + private final Map globalFlagMetadata; + + public StorageQueryResult(FeatureFlag featureFlag, Map globalFlagMetadata) { + this.featureFlag = featureFlag; + this.globalFlagMetadata = globalFlagMetadata; + } +} diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java index 9fcff9746..8401cbe42 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java @@ -451,6 +451,71 @@ void selectorIsOverwrittenByFlagMetadata() throws Exception { assertThat(providerEvaluation.getFlagMetadata().getString("scope")).isEqualTo("new selector"); } + @Test + void globalFlagMetadataIsAddedToEvaluation() throws Exception { + // given + final Map flagMap = new HashMap<>(); + final Map flagMetadata = new HashMap<>(); + flagMetadata.put("scope", "new selector"); + flagMap.put("flag", new FeatureFlag("stage", "loop", stringVariants, "", flagMetadata)); + + final Map globalFlagMetadata = new HashMap<>(); + globalFlagMetadata.put("global", "metadata"); + InProcessResolver inProcessResolver = getInProcessResolverWith( + new MockStorage(flagMap, globalFlagMetadata), connectionEvent -> {}, "selector"); + + // when + ProviderEvaluation providerEvaluation = + inProcessResolver.stringEvaluation("flag", "def", new ImmutableContext()); + + // then + assertThat(providerEvaluation.getFlagMetadata()).isNotNull(); + assertThat(providerEvaluation.getFlagMetadata().getString("scope")).isEqualTo("new selector"); + assertThat(providerEvaluation.getFlagMetadata().getString("global")).isEqualTo("metadata"); + } + + @Test + void globalFlagMetadataIsAddedToFailingEvaluation() throws Exception { + // given + final Map flagMap = new HashMap<>(); + + final Map globalFlagMetadata = new HashMap<>(); + globalFlagMetadata.put("global", "metadata"); + InProcessResolver inProcessResolver = getInProcessResolverWith( + new MockStorage(flagMap, globalFlagMetadata), connectionEvent -> {}, "selector"); + + // when + ProviderEvaluation providerEvaluation = + inProcessResolver.stringEvaluation("does not exist", "def", new ImmutableContext()); + + // then + assertThat(providerEvaluation.getReason()).isNull(); + assertThat(providerEvaluation.getFlagMetadata()).isNotNull(); + assertThat(providerEvaluation.getFlagMetadata().getString("global")).isEqualTo("metadata"); + } + + @Test + void globalFlagMetadataIsOverwrittenByFlagMetadataToEvaluation() throws Exception { + // given + final Map flagMap = new HashMap<>(); + final Map flagMetadata = new HashMap<>(); + flagMetadata.put("key", "expected"); + flagMap.put("flag", new FeatureFlag("stage", "loop", stringVariants, "", flagMetadata)); + + final Map globalFlagMetadata = new HashMap<>(); + globalFlagMetadata.put("key", "unexpected"); + InProcessResolver inProcessResolver = getInProcessResolverWith( + new MockStorage(flagMap, globalFlagMetadata), connectionEvent -> {}, "selector"); + + // when + ProviderEvaluation providerEvaluation = + inProcessResolver.stringEvaluation("flag", "def", new ImmutableContext()); + + // then + assertThat(providerEvaluation.getFlagMetadata()).isNotNull(); + assertThat(providerEvaluation.getFlagMetadata().getString("key")).isEqualTo("expected"); + } + private InProcessResolver getInProcessResolverWith(final FlagdOptions options, final MockStorage storage) throws NoSuchFieldException, IllegalAccessException { diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/MockStorage.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/MockStorage.java index a48a05d12..f4b129dc2 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/MockStorage.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/MockStorage.java @@ -2,7 +2,9 @@ import dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag; import dev.openfeature.contrib.providers.flagd.resolver.process.storage.Storage; +import dev.openfeature.contrib.providers.flagd.resolver.process.storage.StorageQueryResult; import dev.openfeature.contrib.providers.flagd.resolver.process.storage.StorageStateChange; +import java.util.Collections; import java.util.Map; import java.util.concurrent.BlockingQueue; import javax.annotation.Nullable; @@ -10,16 +12,25 @@ public class MockStorage implements Storage { private final Map mockFlags; + private final Map metadata; private final BlockingQueue mockQueue; + public MockStorage(Map mockFlags, Map globalFlagMetadata) { + this.mockFlags = mockFlags; + this.mockQueue = null; + this.metadata = globalFlagMetadata; + } + public MockStorage(Map mockFlags, BlockingQueue mockQueue) { this.mockFlags = mockFlags; this.mockQueue = mockQueue; + this.metadata = Collections.emptyMap(); } public MockStorage(Map flagMap) { this.mockFlags = flagMap; this.mockQueue = null; + this.metadata = Collections.emptyMap(); } public void init() { @@ -30,8 +41,9 @@ public void shutdown() { // no-op } - public FeatureFlag getFlag(String key) { - return mockFlags.get(key); + @Override + public StorageQueryResult getFlag(String key) { + return new StorageQueryResult(mockFlags.get(key), metadata); } @Nullable public BlockingQueue getStateQueue() { diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParserTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParserTest.java index a97d24545..0481e602a 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParserTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParserTest.java @@ -12,6 +12,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.IOException; @@ -21,7 +22,8 @@ class FlagParserTest { @Test void validJsonConfigurationParsing() throws IOException { - Map flagMap = FlagParser.parseString(getFlagsFromResource(VALID_SIMPLE), true); + Map flagMap = + FlagParser.parseString(getFlagsFromResource(VALID_SIMPLE), true).getFlags(); FeatureFlag boolFlag = flagMap.get("myBoolFlag"); assertNotNull(boolFlag); @@ -53,7 +55,8 @@ void validJsonConfigurationParsing() throws IOException { @Test void validJsonConfigurationWithExtraFieldsParsing() throws IOException { - Map flagMap = FlagParser.parseString(getFlagsFromResource(VALID_SIMPLE_EXTRA_FIELD), true); + Map flagMap = FlagParser.parseString(getFlagsFromResource(VALID_SIMPLE_EXTRA_FIELD), true) + .getFlags(); FeatureFlag boolFlag = flagMap.get("myBoolFlag"); assertNotNull(boolFlag); @@ -68,7 +71,8 @@ void validJsonConfigurationWithExtraFieldsParsing() throws IOException { @Test void validJsonConfigurationWithTargetingRulesParsing() throws IOException { - Map flagMap = FlagParser.parseString(getFlagsFromResource(VALID_LONG), true); + Map flagMap = + FlagParser.parseString(getFlagsFromResource(VALID_LONG), true).getFlags(); FeatureFlag stringFlag = flagMap.get("fibAlgo"); assertNotNull(stringFlag); @@ -88,30 +92,40 @@ void validJsonConfigurationWithTargetingRulesParsing() throws IOException { @Test void validJsonConfigurationWithGlobalMetadataParsing() throws IOException { - Map flagMap = - FlagParser.parseString(getFlagsFromResource(VALID_GLOBAL_FLAG_METADATA), true); + ParsingResult parsingResult = FlagParser.parseString(getFlagsFromResource(VALID_GLOBAL_FLAG_METADATA), true); + Map flagMap = parsingResult.getFlags(); FeatureFlag flag = flagMap.get("without-metadata"); assertNotNull(flag); Map metadata = flag.getMetadata(); + Map globalMetadata = parsingResult.getGlobalFlagMetadata(); assertNotNull(metadata); - assertEquals("some string", metadata.get("string")); - assertEquals(true, metadata.get("boolean")); - assertEquals(1.234, metadata.get("float")); + assertNull(metadata.get("string")); + assertNull(metadata.get("boolean")); + assertNull(metadata.get("float")); + assertNotNull(globalMetadata); + assertEquals("some string", globalMetadata.get("string")); + assertEquals(true, globalMetadata.get("boolean")); + assertEquals(1.234, globalMetadata.get("float")); } @Test - void validJsonConfigurationWithGlobalMetadataGetsOverwrittenParsing() throws IOException { - Map flagMap = - FlagParser.parseString(getFlagsFromResource(VALID_GLOBAL_FLAG_METADATA), true); + void validJsonConfigurationWithFlagMetadataParsing() throws IOException { + ParsingResult parsingResult = FlagParser.parseString(getFlagsFromResource(VALID_GLOBAL_FLAG_METADATA), true); + Map flagMap = parsingResult.getFlags(); FeatureFlag flag = flagMap.get("with-metadata"); assertNotNull(flag); Map metadata = flag.getMetadata(); + Map globalMetadata = parsingResult.getGlobalFlagMetadata(); + assertNotNull(globalMetadata); + assertEquals("some string", globalMetadata.get("string")); + assertEquals(true, globalMetadata.get("boolean")); + assertEquals(1.234, globalMetadata.get("float")); assertNotNull(metadata); assertEquals("other string", metadata.get("string")); assertEquals(true, metadata.get("boolean")); diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/FlagStoreTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/FlagStoreTest.java index d0ac07a75..c9e4d1b54 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/FlagStoreTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/FlagStoreTest.java @@ -24,7 +24,7 @@ class FlagStoreTest { @Test - public void connectorHandling() throws Exception { + void connectorHandling() throws Exception { final int maxDelay = 1000; final BlockingQueue payload = new LinkedBlockingQueue<>(); @@ -100,7 +100,7 @@ public void changedFlags() throws Exception { }); // flags changed for first time assertEquals( - FlagParser.parseString(getFlagsFromResource(VALID_SIMPLE), true).keySet().stream() + FlagParser.parseString(getFlagsFromResource(VALID_SIMPLE), true).getFlags().keySet().stream() .collect(Collectors.toList()), storageStateDTOS.take().getChangedFlagsKeys()); @@ -108,7 +108,8 @@ public void changedFlags() throws Exception { payload.offer(new QueuePayload( QueuePayloadType.DATA, getFlagsFromResource(VALID_LONG), GetMetadataResponse.getDefaultInstance())); }); - Map expectedChangedFlags = FlagParser.parseString(getFlagsFromResource(VALID_LONG), true); + Map expectedChangedFlags = + FlagParser.parseString(getFlagsFromResource(VALID_LONG), true).getFlags(); expectedChangedFlags.remove("myBoolFlag"); // flags changed from initial VALID_SIMPLE flag, as a set because we don't care about order Assert.assertEquals( From a8a2cd6f30b26beaba24c3623ac3155d17bfc6b8 Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Fri, 10 Jan 2025 10:47:11 +0100 Subject: [PATCH 09/12] fixup! feat: Update in-process resolver to support flag metadata #1102 Signed-off-by: christian.lutnik --- .../flagd/resolver/process/InProcessResolver.java | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java index 78bf704dd..b8a7c4a1e 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java @@ -41,7 +41,6 @@ public class InProcessResolver implements Resolver { private final Consumer onConnectionEvent; private final Operator operator; private final long deadline; - private final ImmutableMetadata fallBackMetadata; private final Supplier connectedSupplier; private final String scope; @@ -65,14 +64,7 @@ public InProcessResolver( this.onConnectionEvent = onConnectionEvent; this.operator = new Operator(); this.connectedSupplier = connectedSupplier; - if (options.getSelector() == null) { - this.scope = null; - this.fallBackMetadata = null; - } else { - this.scope = options.getSelector(); - this.fallBackMetadata = - ImmutableMetadata.builder().addString("scope", this.scope).build(); - } + this.scope = options.getSelector(); } /** From 1579eff79a935f24f3bbcb8f1812c14b118bf920 Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Tue, 14 Jan 2025 09:21:25 +0100 Subject: [PATCH 10/12] fixup! feat: Update in-process resolver to support flag metadata #1102 Signed-off-by: christian.lutnik --- .../resolver/process/InProcessResolver.java | 2 +- .../resolver/process/model/FlagParser.java | 6 ++-- .../resolver/process/model/ParsingResult.java | 6 ++-- .../resolver/process/storage/FlagStore.java | 12 +++---- .../process/storage/StorageQueryResult.java | 10 +++--- .../process/InProcessResolverTest.java | 28 +++++++-------- .../flagd/resolver/process/MockStorage.java | 12 +++---- .../flagd/resolver/process/TestUtils.java | 4 +-- .../process/model/FlagParserTest.java | 34 +++++++++---------- ...ta.json => invalid-flag-set-metadata.json} | 0 ...data.json => valid-flag-set-metadata.json} | 0 11 files changed, 57 insertions(+), 57 deletions(-) rename providers/flagd/src/test/resources/flagConfigurations/{invalid-global-metadata.json => invalid-flag-set-metadata.json} (100%) rename providers/flagd/src/test/resources/flagConfigurations/{valid-global-metadata.json => valid-flag-set-metadata.json} (100%) diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java index b8a7c4a1e..44d52763c 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java @@ -244,7 +244,7 @@ private ProviderEvaluation resolve(Class type, String key, EvaluationC private ImmutableMetadata getFlagMetadata(StorageQueryResult storageQueryResult) { ImmutableMetadata.ImmutableMetadataBuilder metadataBuilder = ImmutableMetadata.builder(); for (Map.Entry entry : - storageQueryResult.getGlobalFlagMetadata().entrySet()) { + storageQueryResult.getFlagSetMetadata().entrySet()) { addEntryToMetadataBuilder(metadataBuilder, entry.getKey(), entry.getValue()); } diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParser.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParser.java index 7b6a5d4b2..e3eedda6b 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParser.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParser.java @@ -71,12 +71,12 @@ public static ParsingResult parseString(final String configuration, boolean thro final String transposedConfiguration = transposeEvaluators(configuration); final Map flagMap = new HashMap<>(); - final Map metadata; + final Map flagSetMetadata; try (JsonParser parser = MAPPER.createParser(transposedConfiguration)) { final TreeNode treeNode = parser.readValueAsTree(); final TreeNode flagNode = treeNode.get(FLAG_KEY); final TreeNode metadataNode = treeNode.get(METADATA_KEY); - metadata = parseMetadata(metadataNode); + flagSetMetadata = parseMetadata(metadataNode); if (flagNode == null) { throw new IllegalArgumentException("No flag configurations found in the payload"); @@ -89,7 +89,7 @@ public static ParsingResult parseString(final String configuration, boolean thro } } - return new ParsingResult(flagMap, metadata); + return new ParsingResult(flagMap, flagSetMetadata); } private static Map parseMetadata(TreeNode metadataNode) throws JsonProcessingException { diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/ParsingResult.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/ParsingResult.java index c368380e2..485611250 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/ParsingResult.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/ParsingResult.java @@ -13,10 +13,10 @@ justification = "Feature flag comes as a Json configuration, hence they must be exposed") public class ParsingResult { private final Map flags; - private final Map globalFlagMetadata; + private final Map flagSetMetadata; - public ParsingResult(Map flags, Map globalFlagMetadata) { + public ParsingResult(Map flags, Map flagSetMetadata) { this.flags = flags; - this.globalFlagMetadata = globalFlagMetadata; + this.flagSetMetadata = flagSetMetadata; } } diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/FlagStore.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/FlagStore.java index 1b6bd5f47..0645af82a 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/FlagStore.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/FlagStore.java @@ -36,7 +36,7 @@ public class FlagStore implements Storage { private final AtomicBoolean shutdown = new AtomicBoolean(false); private final BlockingQueue stateBlockingQueue = new LinkedBlockingQueue<>(1); private final Map flags = new HashMap<>(); - private final Map globalFlagMetadata = new HashMap<>(); + private final Map flagSetMetadata = new HashMap<>(); private final Connector connector; private final boolean throwIfInvalid; @@ -80,7 +80,7 @@ public void shutdown() throws InterruptedException { connector.shutdown(); } - /** Retrieve flag for the given key and the global flag metadata. */ + /** Retrieve flag for the given key and the flag set metadata. */ @Override public StorageQueryResult getFlag(final String key) { readLock.lock(); @@ -88,7 +88,7 @@ public StorageQueryResult getFlag(final String key) { Map metadata; try { flag = flags.get(key); - metadata = new HashMap<>(globalFlagMetadata); + metadata = new HashMap<>(flagSetMetadata); } finally { readLock.unlock(); } @@ -112,7 +112,7 @@ private void streamerListener(final Connector connector) throws InterruptedExcep List changedFlagsKeys; ParsingResult parsingResult = FlagParser.parseString(payload.getFlagData(), throwIfInvalid); Map flagMap = parsingResult.getFlags(); - Map globalFlagMetadataMap = parsingResult.getGlobalFlagMetadata(); + Map flagSetMetadataMap = parsingResult.getFlagSetMetadata(); Structure metadata = parseSyncMetadata(payload.getMetadataResponse()); writeLock.lock(); @@ -120,8 +120,8 @@ private void streamerListener(final Connector connector) throws InterruptedExcep changedFlagsKeys = getChangedFlagsKeys(flagMap); flags.clear(); flags.putAll(flagMap); - globalFlagMetadata.clear(); - globalFlagMetadata.putAll(globalFlagMetadataMap); + flagSetMetadata.clear(); + flagSetMetadata.putAll(flagSetMetadataMap); } finally { writeLock.unlock(); } diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/StorageQueryResult.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/StorageQueryResult.java index 7474ab873..b0dad0533 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/StorageQueryResult.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/StorageQueryResult.java @@ -7,18 +7,18 @@ /** * To be returned by the storage when a flag is queried. Contains the flag (iff a flag associated with the given key - * exists, null otherwise) and global flag metadata + * exists, null otherwise) and flag set metadata */ @Getter @SuppressFBWarnings( value = {"EI_EXPOSE_REP"}, - justification = "The storage provides access to both feature flags and global metadata") + justification = "The storage provides access to both feature flags and flag set metadata") public class StorageQueryResult { private final FeatureFlag featureFlag; - private final Map globalFlagMetadata; + private final Map flagSetMetadata; - public StorageQueryResult(FeatureFlag featureFlag, Map globalFlagMetadata) { + public StorageQueryResult(FeatureFlag featureFlag, Map flagSetMetadata) { this.featureFlag = featureFlag; - this.globalFlagMetadata = globalFlagMetadata; + this.flagSetMetadata = flagSetMetadata; } } diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java index 8401cbe42..12ef3be44 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java @@ -452,17 +452,17 @@ void selectorIsOverwrittenByFlagMetadata() throws Exception { } @Test - void globalFlagMetadataIsAddedToEvaluation() throws Exception { + void flagSetMetadataIsAddedToEvaluation() throws Exception { // given final Map flagMap = new HashMap<>(); final Map flagMetadata = new HashMap<>(); flagMetadata.put("scope", "new selector"); flagMap.put("flag", new FeatureFlag("stage", "loop", stringVariants, "", flagMetadata)); - final Map globalFlagMetadata = new HashMap<>(); - globalFlagMetadata.put("global", "metadata"); + final Map flagSetMetadata = new HashMap<>(); + flagSetMetadata.put("flagSetMetadata", "metadata"); InProcessResolver inProcessResolver = getInProcessResolverWith( - new MockStorage(flagMap, globalFlagMetadata), connectionEvent -> {}, "selector"); + new MockStorage(flagMap, flagSetMetadata), connectionEvent -> {}, "selector"); // when ProviderEvaluation providerEvaluation = @@ -471,18 +471,18 @@ void globalFlagMetadataIsAddedToEvaluation() throws Exception { // then assertThat(providerEvaluation.getFlagMetadata()).isNotNull(); assertThat(providerEvaluation.getFlagMetadata().getString("scope")).isEqualTo("new selector"); - assertThat(providerEvaluation.getFlagMetadata().getString("global")).isEqualTo("metadata"); + assertThat(providerEvaluation.getFlagMetadata().getString("flagSetMetadata")).isEqualTo("metadata"); } @Test - void globalFlagMetadataIsAddedToFailingEvaluation() throws Exception { + void flagSetMetadataIsAddedToFailingEvaluation() throws Exception { // given final Map flagMap = new HashMap<>(); - final Map globalFlagMetadata = new HashMap<>(); - globalFlagMetadata.put("global", "metadata"); + final Map flagSetMetadata = new HashMap<>(); + flagSetMetadata.put("flagSetMetadata", "metadata"); InProcessResolver inProcessResolver = getInProcessResolverWith( - new MockStorage(flagMap, globalFlagMetadata), connectionEvent -> {}, "selector"); + new MockStorage(flagMap, flagSetMetadata), connectionEvent -> {}, "selector"); // when ProviderEvaluation providerEvaluation = @@ -491,21 +491,21 @@ void globalFlagMetadataIsAddedToFailingEvaluation() throws Exception { // then assertThat(providerEvaluation.getReason()).isNull(); assertThat(providerEvaluation.getFlagMetadata()).isNotNull(); - assertThat(providerEvaluation.getFlagMetadata().getString("global")).isEqualTo("metadata"); + assertThat(providerEvaluation.getFlagMetadata().getString("flagSetMetadata")).isEqualTo("metadata"); } @Test - void globalFlagMetadataIsOverwrittenByFlagMetadataToEvaluation() throws Exception { + void flagSetMetadataIsOverwrittenByFlagMetadataToEvaluation() throws Exception { // given final Map flagMap = new HashMap<>(); final Map flagMetadata = new HashMap<>(); flagMetadata.put("key", "expected"); flagMap.put("flag", new FeatureFlag("stage", "loop", stringVariants, "", flagMetadata)); - final Map globalFlagMetadata = new HashMap<>(); - globalFlagMetadata.put("key", "unexpected"); + final Map flagSetMetadata = new HashMap<>(); + flagSetMetadata.put("key", "unexpected"); InProcessResolver inProcessResolver = getInProcessResolverWith( - new MockStorage(flagMap, globalFlagMetadata), connectionEvent -> {}, "selector"); + new MockStorage(flagMap, flagSetMetadata), connectionEvent -> {}, "selector"); // when ProviderEvaluation providerEvaluation = diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/MockStorage.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/MockStorage.java index f4b129dc2..5e5d4b199 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/MockStorage.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/MockStorage.java @@ -12,25 +12,25 @@ public class MockStorage implements Storage { private final Map mockFlags; - private final Map metadata; + private final Map flagSetMetadata; private final BlockingQueue mockQueue; - public MockStorage(Map mockFlags, Map globalFlagMetadata) { + public MockStorage(Map mockFlags, Map flagSetMetadata) { this.mockFlags = mockFlags; this.mockQueue = null; - this.metadata = globalFlagMetadata; + this.flagSetMetadata = flagSetMetadata; } public MockStorage(Map mockFlags, BlockingQueue mockQueue) { this.mockFlags = mockFlags; this.mockQueue = mockQueue; - this.metadata = Collections.emptyMap(); + this.flagSetMetadata = Collections.emptyMap(); } public MockStorage(Map flagMap) { this.mockFlags = flagMap; this.mockQueue = null; - this.metadata = Collections.emptyMap(); + this.flagSetMetadata = Collections.emptyMap(); } public void init() { @@ -43,7 +43,7 @@ public void shutdown() { @Override public StorageQueryResult getFlag(String key) { - return new StorageQueryResult(mockFlags.get(key), metadata); + return new StorageQueryResult(mockFlags.get(key), flagSetMetadata); } @Nullable public BlockingQueue getStateQueue() { diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/TestUtils.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/TestUtils.java index e042888be..90e4ba8d8 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/TestUtils.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/TestUtils.java @@ -15,8 +15,8 @@ public class TestUtils { public static final String VALID_LONG = "flagConfigurations/valid-long.json"; public static final String INVALID_FLAG = "flagConfigurations/invalid-flag.json"; public static final String INVALID_FLAG_METADATA = "flagConfigurations/invalid-metadata.json"; - public static final String INVALID_GLOBAL_FLAG_METADATA = "flagConfigurations/invalid-global-metadata.json"; - public static final String VALID_GLOBAL_FLAG_METADATA = "flagConfigurations/valid-global-metadata.json"; + public static final String INVALID_FLAG_SET_METADATA = "flagConfigurations/invalid-flag-set-metadata.json"; + public static final String VALID_FLAG_SET_METADATA = "flagConfigurations/valid-flag-set-metadata.json"; public static final String INVALID_CFG = "flagConfigurations/invalid-configuration.json"; public static final String UPDATABLE_FILE = "flagConfigurations/updatableFlags.json"; diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParserTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParserTest.java index 0481e602a..79ee279fd 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParserTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FlagParserTest.java @@ -3,8 +3,8 @@ import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.INVALID_CFG; import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.INVALID_FLAG; import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.INVALID_FLAG_METADATA; -import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.INVALID_GLOBAL_FLAG_METADATA; -import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.VALID_GLOBAL_FLAG_METADATA; +import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.INVALID_FLAG_SET_METADATA; +import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.VALID_FLAG_SET_METADATA; import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.VALID_LONG; import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.VALID_SIMPLE; import static dev.openfeature.contrib.providers.flagd.resolver.process.TestUtils.VALID_SIMPLE_EXTRA_FIELD; @@ -91,41 +91,41 @@ void validJsonConfigurationWithTargetingRulesParsing() throws IOException { } @Test - void validJsonConfigurationWithGlobalMetadataParsing() throws IOException { - ParsingResult parsingResult = FlagParser.parseString(getFlagsFromResource(VALID_GLOBAL_FLAG_METADATA), true); + void validJsonConfigurationWithFlagSetMetadataParsing() throws IOException { + ParsingResult parsingResult = FlagParser.parseString(getFlagsFromResource(VALID_FLAG_SET_METADATA), true); Map flagMap = parsingResult.getFlags(); FeatureFlag flag = flagMap.get("without-metadata"); assertNotNull(flag); Map metadata = flag.getMetadata(); - Map globalMetadata = parsingResult.getGlobalFlagMetadata(); + Map flagSetMetadata = parsingResult.getFlagSetMetadata(); assertNotNull(metadata); assertNull(metadata.get("string")); assertNull(metadata.get("boolean")); assertNull(metadata.get("float")); - assertNotNull(globalMetadata); - assertEquals("some string", globalMetadata.get("string")); - assertEquals(true, globalMetadata.get("boolean")); - assertEquals(1.234, globalMetadata.get("float")); + assertNotNull(flagSetMetadata); + assertEquals("some string", flagSetMetadata.get("string")); + assertEquals(true, flagSetMetadata.get("boolean")); + assertEquals(1.234, flagSetMetadata.get("float")); } @Test void validJsonConfigurationWithFlagMetadataParsing() throws IOException { - ParsingResult parsingResult = FlagParser.parseString(getFlagsFromResource(VALID_GLOBAL_FLAG_METADATA), true); + ParsingResult parsingResult = FlagParser.parseString(getFlagsFromResource(VALID_FLAG_SET_METADATA), true); Map flagMap = parsingResult.getFlags(); FeatureFlag flag = flagMap.get("with-metadata"); assertNotNull(flag); Map metadata = flag.getMetadata(); - Map globalMetadata = parsingResult.getGlobalFlagMetadata(); + Map flagSetMetadata = parsingResult.getFlagSetMetadata(); - assertNotNull(globalMetadata); - assertEquals("some string", globalMetadata.get("string")); - assertEquals(true, globalMetadata.get("boolean")); - assertEquals(1.234, globalMetadata.get("float")); + assertNotNull(flagSetMetadata); + assertEquals("some string", flagSetMetadata.get("string")); + assertEquals(true, flagSetMetadata.get("boolean")); + assertEquals(1.234, flagSetMetadata.get("float")); assertNotNull(metadata); assertEquals("other string", metadata.get("string")); assertEquals(true, metadata.get("boolean")); @@ -145,8 +145,8 @@ void invalidFlagMetadataThrowsError() throws IOException { } @Test - void invalidGlobalFlagMetadataThrowsError() throws IOException { - String flagString = getFlagsFromResource(INVALID_GLOBAL_FLAG_METADATA); + void invalidFlagSetMetadataThrowsError() throws IOException { + String flagString = getFlagsFromResource(INVALID_FLAG_SET_METADATA); assertThrows(IllegalArgumentException.class, () -> FlagParser.parseString(flagString, true)); } diff --git a/providers/flagd/src/test/resources/flagConfigurations/invalid-global-metadata.json b/providers/flagd/src/test/resources/flagConfigurations/invalid-flag-set-metadata.json similarity index 100% rename from providers/flagd/src/test/resources/flagConfigurations/invalid-global-metadata.json rename to providers/flagd/src/test/resources/flagConfigurations/invalid-flag-set-metadata.json diff --git a/providers/flagd/src/test/resources/flagConfigurations/valid-global-metadata.json b/providers/flagd/src/test/resources/flagConfigurations/valid-flag-set-metadata.json similarity index 100% rename from providers/flagd/src/test/resources/flagConfigurations/valid-global-metadata.json rename to providers/flagd/src/test/resources/flagConfigurations/valid-flag-set-metadata.json From 0551684f4a3db41d2010da5ba82d233345a78bfd Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Tue, 14 Jan 2025 09:27:37 +0100 Subject: [PATCH 11/12] fixup! feat: Update in-process resolver to support flag metadata #1102 Signed-off-by: christian.lutnik --- .../process/InProcessResolverTest.java | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java index 12ef3be44..6cd2f36dd 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolverTest.java @@ -461,8 +461,8 @@ void flagSetMetadataIsAddedToEvaluation() throws Exception { final Map flagSetMetadata = new HashMap<>(); flagSetMetadata.put("flagSetMetadata", "metadata"); - InProcessResolver inProcessResolver = getInProcessResolverWith( - new MockStorage(flagMap, flagSetMetadata), connectionEvent -> {}, "selector"); + InProcessResolver inProcessResolver = + getInProcessResolverWith(new MockStorage(flagMap, flagSetMetadata), connectionEvent -> {}, "selector"); // when ProviderEvaluation providerEvaluation = @@ -471,7 +471,8 @@ void flagSetMetadataIsAddedToEvaluation() throws Exception { // then assertThat(providerEvaluation.getFlagMetadata()).isNotNull(); assertThat(providerEvaluation.getFlagMetadata().getString("scope")).isEqualTo("new selector"); - assertThat(providerEvaluation.getFlagMetadata().getString("flagSetMetadata")).isEqualTo("metadata"); + assertThat(providerEvaluation.getFlagMetadata().getString("flagSetMetadata")) + .isEqualTo("metadata"); } @Test @@ -481,8 +482,8 @@ void flagSetMetadataIsAddedToFailingEvaluation() throws Exception { final Map flagSetMetadata = new HashMap<>(); flagSetMetadata.put("flagSetMetadata", "metadata"); - InProcessResolver inProcessResolver = getInProcessResolverWith( - new MockStorage(flagMap, flagSetMetadata), connectionEvent -> {}, "selector"); + InProcessResolver inProcessResolver = + getInProcessResolverWith(new MockStorage(flagMap, flagSetMetadata), connectionEvent -> {}, "selector"); // when ProviderEvaluation providerEvaluation = @@ -491,7 +492,8 @@ void flagSetMetadataIsAddedToFailingEvaluation() throws Exception { // then assertThat(providerEvaluation.getReason()).isNull(); assertThat(providerEvaluation.getFlagMetadata()).isNotNull(); - assertThat(providerEvaluation.getFlagMetadata().getString("flagSetMetadata")).isEqualTo("metadata"); + assertThat(providerEvaluation.getFlagMetadata().getString("flagSetMetadata")) + .isEqualTo("metadata"); } @Test @@ -504,8 +506,8 @@ void flagSetMetadataIsOverwrittenByFlagMetadataToEvaluation() throws Exception { final Map flagSetMetadata = new HashMap<>(); flagSetMetadata.put("key", "unexpected"); - InProcessResolver inProcessResolver = getInProcessResolverWith( - new MockStorage(flagMap, flagSetMetadata), connectionEvent -> {}, "selector"); + InProcessResolver inProcessResolver = + getInProcessResolverWith(new MockStorage(flagMap, flagSetMetadata), connectionEvent -> {}, "selector"); // when ProviderEvaluation providerEvaluation = From b0868e0fa81e2f3b8716b018d4ca4abbb24e1223 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Tue, 14 Jan 2025 14:06:57 -0500 Subject: [PATCH 12/12] fixup: more conflicts Signed-off-by: Todd Baert --- .../providers/flagd/resolver/process/InProcessResolver.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java index 3f5129eea..95f2eb6d6 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java @@ -10,11 +10,8 @@ import dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag; import dev.openfeature.contrib.providers.flagd.resolver.process.storage.FlagStore; import dev.openfeature.contrib.providers.flagd.resolver.process.storage.Storage; -<<<<<<< HEAD import dev.openfeature.contrib.providers.flagd.resolver.process.storage.StorageQueryResult; -======= import dev.openfeature.contrib.providers.flagd.resolver.process.storage.StorageState; ->>>>>>> main import dev.openfeature.contrib.providers.flagd.resolver.process.storage.StorageStateChange; import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.Connector; import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.file.FileConnector;