From 7113f97a4d99f9cc6d961ebba89fb6d288165de0 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 15 Feb 2023 16:19:45 -0800 Subject: [PATCH 01/10] Scaffold and implement value conversion. --- build.gradle | 3 + .../EvaluationContextConverter.java | 63 +++++++++++++++ .../EvaluationDetailConverter.java | 7 ++ .../serverprovider/ValueConverter.java | 65 ++++++++++++++++ src/test/java/LibraryTest.java | 12 --- .../serverprovider/GivenAValueConverter.java | 77 +++++++++++++++++++ 6 files changed, 215 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/launchdarkly/openfeature/serverprovider/EvaluationContextConverter.java create mode 100644 src/main/java/com/launchdarkly/openfeature/serverprovider/EvaluationDetailConverter.java create mode 100644 src/main/java/com/launchdarkly/openfeature/serverprovider/ValueConverter.java delete mode 100644 src/test/java/LibraryTest.java create mode 100644 src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAValueConverter.java diff --git a/build.gradle b/build.gradle index 74c19b7..b1248be 100644 --- a/build.gradle +++ b/build.gradle @@ -23,6 +23,9 @@ dependencies { // This dependency is used internally, and not exposed to consumers on their own compile classpath. implementation 'com.google.guava:guava:23.0' + implementation group: 'com.launchdarkly', name: 'launchdarkly-java-server-sdk', version: '6.0.0' + implementation 'dev.openfeature:sdk:1.2.0' + // Use JUnit test framework testImplementation 'junit:junit:4.12' } diff --git a/src/main/java/com/launchdarkly/openfeature/serverprovider/EvaluationContextConverter.java b/src/main/java/com/launchdarkly/openfeature/serverprovider/EvaluationContextConverter.java new file mode 100644 index 0000000..3b13f6a --- /dev/null +++ b/src/main/java/com/launchdarkly/openfeature/serverprovider/EvaluationContextConverter.java @@ -0,0 +1,63 @@ +package com.launchdarkly.openfeature.serverprovider; + +import com.launchdarkly.logging.LDLogger; +import com.launchdarkly.sdk.ContextBuilder; +import com.launchdarkly.sdk.ContextKind; +import com.launchdarkly.sdk.LDContext; +import dev.openfeature.sdk.EvaluationContext; +import dev.openfeature.sdk.Value; + +import java.util.HashMap; +import java.util.Map; + +/** + * Converts an OpenFeature EvaluationContext into a LDContext. + */ +class EvaluationContextConverter { + private final LDLogger logger; + + EvaluationContextConverter(LDLogger logger) { + this.logger = logger; + } + + public LDContext toLdContext(EvaluationContext evaluationContext) { + Value kindAsValue = evaluationContext.getValue("kind"); + + String finalKind = "user"; + if(kindAsValue != null && kindAsValue.isString()) { + String kindString = kindAsValue.asString(); + if(kindString == "multi") { + // A multi-context. + + } else { + // Single context with specified kind. + finalKind = kindString; + } + } else if(kindAsValue != null) { + logger.error("The evaluation context contained an invalid kind."); + } + // No kind specified, so it is a user kind. + + String targetingKey = evaluationContext.getTargetingKey(); + Value keyAsValue = evaluationContext.getValue("key"); + + if(targetingKey != null && keyAsValue != null && keyAsValue.isString()) { + // There is both a targeting key and a key. It will work, but probably + // is not intentional. + logger.warn("EvaluationContext contained both a 'key' and 'targetingKey'."); + } + + // Targeting key takes precedence over key, because targeting key is in the spec. + targetingKey = targetingKey != null ? targetingKey : keyAsValue.asString(); + + return BuildSingleContext(evaluationContext.asMap(), finalKind, targetingKey); + } + + private LDContext BuildSingleContext(Map attributes, String kind, String key) { + ContextBuilder builder = LDContext.builder(ContextKind.of(kind), key); + + // TODO: Use the attributes. + + return builder.build(); + } +} diff --git a/src/main/java/com/launchdarkly/openfeature/serverprovider/EvaluationDetailConverter.java b/src/main/java/com/launchdarkly/openfeature/serverprovider/EvaluationDetailConverter.java new file mode 100644 index 0000000..de3254c --- /dev/null +++ b/src/main/java/com/launchdarkly/openfeature/serverprovider/EvaluationDetailConverter.java @@ -0,0 +1,7 @@ +package com.launchdarkly.openfeature.serverprovider; + +/** + * Converts an EvaluationDetail into an OpenFeature ResolutionDetails. + */ +class EvaluationDetailConverter { +} diff --git a/src/main/java/com/launchdarkly/openfeature/serverprovider/ValueConverter.java b/src/main/java/com/launchdarkly/openfeature/serverprovider/ValueConverter.java new file mode 100644 index 0000000..8e889c7 --- /dev/null +++ b/src/main/java/com/launchdarkly/openfeature/serverprovider/ValueConverter.java @@ -0,0 +1,65 @@ +package com.launchdarkly.openfeature.serverprovider; + +import com.launchdarkly.logging.LDLogger; +import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.ObjectBuilder; +import dev.openfeature.sdk.Structure; +import dev.openfeature.sdk.Value; + +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Converts an OpenFeature Value into an LDValue. + */ +class ValueConverter { + private final LDLogger logger; + + public ValueConverter(LDLogger logger) { + this.logger = logger; + } + + public LDValue toLdValue(Value value) { + if(value.isNull()) { + return LDValue.ofNull(); + } + if(value.isBoolean()) { + return LDValue.of(value.asBoolean()); + } + if(value.isNumber()) { + return LDValue.of(value.asDouble()); + } + if(value.isString()) { + return LDValue.of(value.asString()); + } + if(value.isInstant()) { + DateTimeFormatter formatter = DateTimeFormatter + .ISO_DATE_TIME + .withZone(ZoneId.from(ZoneOffset.UTC)); + return LDValue.of(formatter.format(value.asInstant())); + } + if(value.isList()) { + List asList = value.asList(); + List asLdValues = asList.stream() + .map(this::toLdValue) + .collect(Collectors.toList()); + return LDValue.arrayOf(asLdValues.toArray(new LDValue[0])); + } + if(value.isStructure()) { + ObjectBuilder objectBuilder = LDValue.buildObject(); + Structure structure = value.asStructure(); + structure.asMap().forEach((itemKey, itemValue) -> { + LDValue itemLdValue = toLdValue(itemValue); + objectBuilder.put(itemKey, itemLdValue); + }); + return objectBuilder.build(); + } + + // Could not convert, should not happen. + logger.error("Could not convert Value in context to LDValue"); + return LDValue.ofNull(); + } +} diff --git a/src/test/java/LibraryTest.java b/src/test/java/LibraryTest.java deleted file mode 100644 index 645740d..0000000 --- a/src/test/java/LibraryTest.java +++ /dev/null @@ -1,12 +0,0 @@ -/* - * This Java source file was generated by the Gradle 'init' task. - */ -import org.junit.Test; -import static org.junit.Assert.*; - -public class LibraryTest { - @Test public void testSomeLibraryMethod() { - Library classUnderTest = new Library(); - assertTrue("someLibraryMethod should return 'true'", classUnderTest.someLibraryMethod()); - } -} diff --git a/src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAValueConverter.java b/src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAValueConverter.java new file mode 100644 index 0000000..708edea --- /dev/null +++ b/src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAValueConverter.java @@ -0,0 +1,77 @@ +package com.launchdarkly.openfeature.serverprovider;/* + * This Java source file was generated by the Gradle 'init' task. + */ +import com.launchdarkly.logging.LDLogger; +import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.LDValueType; +import dev.openfeature.sdk.Value; +import org.junit.Test; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.*; + +public class GivenAValueConverter { + private ValueConverter valueConverter = new ValueConverter(LDLogger.none()); + private final Double EPSILON = 0.00001; + + @Test public void itCanConvertNull() { + LDValue value = valueConverter.toLdValue(new Value()); + assertTrue(value.isNull()); + } + + @Test public void itCanConvertBooleans() { + LDValue trueValue = valueConverter.toLdValue(new Value(true)); + assertTrue(trueValue.booleanValue()); + assertEquals(trueValue.getType(), LDValueType.BOOLEAN); + + LDValue falseValue = valueConverter.toLdValue(new Value(false)); + assertFalse(falseValue.booleanValue()); + assertEquals(falseValue.getType(), LDValueType.BOOLEAN); + } + + @Test public void itCanConvertNumbers() { + LDValue zeroValue = valueConverter.toLdValue(new Value(0)); + assertEquals(0.0, zeroValue.doubleValue(), EPSILON); + assertTrue(zeroValue.isNumber()); + + LDValue numberValue = valueConverter.toLdValue(new Value(1000)); + assertEquals(numberValue.doubleValue(), 1000.0, EPSILON); + assertTrue(numberValue.isNumber()); + } + + @Test public void itCanConvertStrings() { + LDValue stringValue = valueConverter.toLdValue(new Value("the string")); + assertTrue(stringValue.isString()); + assertEquals("the string", stringValue.stringValue()); + } + + @Test public void itCanConvertInstants() { + LDValue dateString = valueConverter.toLdValue(new Value(Instant.ofEpochMilli(0))); + assertEquals("1970-01-01T00:00:00Z", dateString.stringValue()); + } + + @Test public void itCanConvertLists() { + Value ofValueList = new Value(new ArrayList(){{ + add(new Value(true)); + add(new Value(false)); + add(new Value(17)); + add(new Value(42.5)); + add(new Value("string")); + }}); + + LDValue ldValue = valueConverter.toLdValue(ofValueList); + List ldValueList = new ArrayList(); + ldValue.values().forEach(ldValueList::add); + + assertEquals(5, ldValueList.size()); + + assertTrue(ldValueList.get(0).booleanValue()); + assertFalse(ldValueList.get(1).booleanValue()); + assertEquals(17.0, ldValueList.get(2).doubleValue(), EPSILON); + assertEquals(42.5, ldValueList.get(3).doubleValue(), EPSILON); + assertEquals("string", ldValueList.get(4).stringValue()); + } +} From 1e2819efe30fd16217e4a5f63c6359545c73b50f Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 15 Feb 2023 18:54:40 -0800 Subject: [PATCH 02/10] Continue work on context conversion. --- src/main/java/Library.java | 8 -- .../EvaluationContextConverter.java | 103 ++++++++++++++++-- .../GivenAContextConverter.java | 39 +++++++ .../serverprovider/GivenAValueConverter.java | 5 +- 4 files changed, 136 insertions(+), 19 deletions(-) delete mode 100644 src/main/java/Library.java create mode 100644 src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAContextConverter.java diff --git a/src/main/java/Library.java b/src/main/java/Library.java deleted file mode 100644 index fa1b41a..0000000 --- a/src/main/java/Library.java +++ /dev/null @@ -1,8 +0,0 @@ -/* - * This Java source file was generated by the Gradle 'init' task. - */ -public class Library { - public boolean someLibraryMethod() { - return true; - } -} diff --git a/src/main/java/com/launchdarkly/openfeature/serverprovider/EvaluationContextConverter.java b/src/main/java/com/launchdarkly/openfeature/serverprovider/EvaluationContextConverter.java index 3b13f6a..e41d387 100644 --- a/src/main/java/com/launchdarkly/openfeature/serverprovider/EvaluationContextConverter.java +++ b/src/main/java/com/launchdarkly/openfeature/serverprovider/EvaluationContextConverter.java @@ -3,32 +3,41 @@ import com.launchdarkly.logging.LDLogger; import com.launchdarkly.sdk.ContextBuilder; import com.launchdarkly.sdk.ContextKind; +import com.launchdarkly.sdk.ContextMultiBuilder; import com.launchdarkly.sdk.LDContext; import dev.openfeature.sdk.EvaluationContext; import dev.openfeature.sdk.Value; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.stream.Collectors; /** * Converts an OpenFeature EvaluationContext into a LDContext. */ class EvaluationContextConverter { private final LDLogger logger; + private final ValueConverter valueConverter; - EvaluationContextConverter(LDLogger logger) { + public EvaluationContextConverter(LDLogger logger) { this.logger = logger; + this.valueConverter = new ValueConverter(logger); } public LDContext toLdContext(EvaluationContext evaluationContext) { - Value kindAsValue = evaluationContext.getValue("kind"); + // Using the kind as a map here because getting a value from an immutable context that doesn't exist + // throws. https://github.com/open-feature/java-sdk/pull/300 + Map attributes = evaluationContext.asMap(); + + Value kindAsValue = attributes.get("kind"); String finalKind = "user"; if(kindAsValue != null && kindAsValue.isString()) { String kindString = kindAsValue.asString(); if(kindString == "multi") { // A multi-context. - + return BuildMultiContext(evaluationContext); } else { // Single context with specified kind. finalKind = kindString; @@ -39,24 +48,102 @@ public LDContext toLdContext(EvaluationContext evaluationContext) { // No kind specified, so it is a user kind. String targetingKey = evaluationContext.getTargetingKey(); - Value keyAsValue = evaluationContext.getValue("key"); + Value keyAsValue = attributes.get("key"); + + targetingKey = getTargetingKey(targetingKey, keyAsValue); + + return BuildSingleContext(evaluationContext.asMap(), finalKind, targetingKey); + } - if(targetingKey != null && keyAsValue != null && keyAsValue.isString()) { + /** + * Get and validate a targeting key. + * @param targetingKey Targeting key as a string, or null. + * @param keyAsValue Key as a Value, or null. + * @return Returns a key, or null if one is not available. + */ + private String getTargetingKey(String targetingKey, Value keyAsValue) { + // Currently the targeting key will always have a value, but it can be empty. + // So we want to tread an empty string as a not defined one. Later it could + // become null, so we will need to check that. + if(targetingKey != "" && keyAsValue != null && keyAsValue.isString()) { // There is both a targeting key and a key. It will work, but probably // is not intentional. logger.warn("EvaluationContext contained both a 'key' and 'targetingKey'."); } + if(keyAsValue != null && !keyAsValue.isString()) { + logger.warn("A non-string 'key' attribute was provided."); + } + // Targeting key takes precedence over key, because targeting key is in the spec. - targetingKey = targetingKey != null ? targetingKey : keyAsValue.asString(); + targetingKey = targetingKey != "" ? targetingKey : keyAsValue.asString(); - return BuildSingleContext(evaluationContext.asMap(), finalKind, targetingKey); + if(targetingKey == null) { + logger.error("The EvaluationContext must contain either a 'targetingKey' or a 'key' and the type" + + "must be a string."); + } + return targetingKey; + } + + private LDContext BuildMultiContext(EvaluationContext evaluationContext) { + ContextMultiBuilder multiBuilder = LDContext.multiBuilder(); + + evaluationContext.asMap().forEach((kind, attributes) -> { + // Do not need to do anything for the kind key. + if(kind == "kind") return; + + if(!attributes.isStructure()) { + // The attributes need to be a structure to be part of a multi-context. + logger.warn("Top level attributes in a multi-kind context should be Structure types."); + return; + } + + Map attributesMap = attributes.asStructure().asMap(); + Value keyAsValue = attributesMap.get("key"); + String targetingKey = attributesMap.get("targetingKey").toString(); + targetingKey = getTargetingKey(targetingKey, keyAsValue); + + LDContext singleContext = BuildSingleContext(attributesMap, kind, targetingKey); + multiBuilder.add(singleContext); + }); + return multiBuilder.build(); } private LDContext BuildSingleContext(Map attributes, String kind, String key) { ContextBuilder builder = LDContext.builder(ContextKind.of(kind), key); - // TODO: Use the attributes. + attributes.forEach((attrKey, attrValue) -> { + // Key has been processed, so we can skip it. + if(attrKey == "key" || attrKey == "targetingKey") return; + + if(attrKey == "privateAttributes") { + List valueList = attrValue.asList(); + if(valueList == null) { + logger.error("A key of 'privateAttributes' in an evaluation context must have a list value."); + return; + } + boolean allStrings = valueList.stream().allMatch(item -> item.isString()); + if(!allStrings) { + logger.error("A key of 'privateAttributes' must be a list of only string values."); + return; + } + + builder.privateAttributes( + valueList.stream() + .map(item -> item.asString()) + .collect(Collectors.toList()) + .toArray(new String[0])); + return; + } + if(attrKey == "anonymous") { + // TODO: Anonymous stuff. + } + if(attrKey == "name") { + // TODO: Name stuff. + } + + builder.set(attrKey, valueConverter.toLdValue(attrValue)); + }); return builder.build(); } diff --git a/src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAContextConverter.java b/src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAContextConverter.java new file mode 100644 index 0000000..865e67f --- /dev/null +++ b/src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAContextConverter.java @@ -0,0 +1,39 @@ +package com.launchdarkly.openfeature.serverprovider; + +import com.launchdarkly.logging.LDLogger; +import com.launchdarkly.sdk.ContextKind; +import com.launchdarkly.sdk.LDContext; +import dev.openfeature.sdk.EvaluationContext; +import dev.openfeature.sdk.ImmutableContext; +import dev.openfeature.sdk.Value; +import org.junit.Test; + +import java.util.HashMap; + +import static org.junit.Assert.*; + +public class GivenAContextConverter { + EvaluationContextConverter evaluationContextConverter = new EvaluationContextConverter(LDLogger.none()); + + @Test public void itCanCreateAContextFromAKeyOnly() { + LDContext expectedContext = LDContext.builder("the-key").build(); + LDContext converted = evaluationContextConverter.toLdContext(new ImmutableContext("the-key")); + assertEquals(expectedContext, converted); + + HashMap attributes = new HashMap(); + attributes.put("key", new Value("the-key")); + LDContext convertedKey = evaluationContextConverter.toLdContext(new ImmutableContext(attributes)); + assertEquals(expectedContext, convertedKey); + } + + @Test public void itCanCreateAContextFromAKeyAndKind() { + LDContext expectedContext = LDContext.builder(ContextKind.of("organization"), "org-key").build(); + + HashMap attributes = new HashMap(); + attributes.put("kind", new Value("organization")); + LDContext converted = evaluationContextConverter + .toLdContext(new ImmutableContext("org-key", attributes)); + + assertEquals(expectedContext, converted); + } +} diff --git a/src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAValueConverter.java b/src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAValueConverter.java index 708edea..ae28462 100644 --- a/src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAValueConverter.java +++ b/src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAValueConverter.java @@ -1,6 +1,5 @@ -package com.launchdarkly.openfeature.serverprovider;/* - * This Java source file was generated by the Gradle 'init' task. - */ +package com.launchdarkly.openfeature.serverprovider; + import com.launchdarkly.logging.LDLogger; import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.LDValueType; From 3276705960722323032423c6fe769c62dfc3ec48 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Thu, 16 Feb 2023 10:58:34 -0800 Subject: [PATCH 03/10] Add support for evaluation context conversion. --- README.md | 47 +++++- .../EvaluationContextConverter.java | 88 +++++++---- .../serverprovider/ValueConverter.java | 14 +- .../GivenAContextConverter.java | 142 +++++++++++++++++- .../serverprovider/GivenAValueConverter.java | 20 ++- .../serverprovider/TestLogger.java | 91 +++++++++++ 6 files changed, 350 insertions(+), 52 deletions(-) create mode 100644 src/test/java/com/launchdarkly/openfeature/serverprovider/TestLogger.java diff --git a/README.md b/README.md index d16485e..f43294b 100644 --- a/README.md +++ b/README.md @@ -54,31 +54,68 @@ There are several other attributes which have special functionality within a sin #### A single user context ```java -// TODO: Write + EvaluationContext context = new ImmutableContext("the-key"); ``` #### A single context of kind "organization" ```java -// TODO: Write + EvaluationContext context = new ImmutableContext("org-key", new HashMap(){{ + put("kind", new Value("organization")); + }}); ``` #### A multi-context containing a "user" and an "organization" ```java -// TODO: Write +EvaluationContext context = new ImmutableContext(new HashMap() {{ + put("kind", new Value("multi")); + put("organization", new Value(new ImmutableStructure(new HashMap(){{ + put("name", new Value("the-org-name")); + put("targetingKey", new Value("my-org-key")); + put("myCustomAttribute", new Value("myAttributeValue")); + }}))); + put("user", new Value(new ImmutableStructure(new HashMap(){{ + put("key", new Value("my-user-key")); + put("anonymous", new Value(true)); + }}))); +}}); ``` #### Setting private attributes in a single context ```java -// TODO: Write + EvaluationContext context = new ImmutableContext("org-key", new HashMap(){{ + put("kind", new Value("organization")); + put("myCustomAttribute", new Value("myAttributeValue")); + put("privateAttributes", new Value(new ArrayList() {{ + add(new Value("myCustomAttribute")); + }})); + }}); ``` #### Setting private attributes in a multi-context ```java -// TODO: Write +EvaluationContext evaluationContext = new ImmutableContext(new HashMap() {{ + put("kind", new Value("multi")); + put("organization", new Value(new ImmutableStructure(new HashMap(){{ + put("name", new Value("the-org-name")); + put("targetingKey", new Value("my-org-key")); + // This will ONLY apply to the "organization" attributes. + put("privateAttributes", new Value(new ArrayList() {{ + add(new Value("myCustomAttribute")); + }})); + // This attribute will be private. + put("myCustomAttribute", new Value("myAttributeValue")); + }}))); + put("user", new Value(new ImmutableStructure(new HashMap(){{ + put("key", new Value("my-user-key")); + put("anonymous", new Value(true)); + // This attribute will not be private. + put("myCustomAttribute", new Value("myAttributeValue")); + }}))); +}}); ``` ## Learn more diff --git a/src/main/java/com/launchdarkly/openfeature/serverprovider/EvaluationContextConverter.java b/src/main/java/com/launchdarkly/openfeature/serverprovider/EvaluationContextConverter.java index e41d387..aaa92c0 100644 --- a/src/main/java/com/launchdarkly/openfeature/serverprovider/EvaluationContextConverter.java +++ b/src/main/java/com/launchdarkly/openfeature/serverprovider/EvaluationContextConverter.java @@ -8,7 +8,6 @@ import dev.openfeature.sdk.EvaluationContext; import dev.openfeature.sdk.Value; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -25,6 +24,15 @@ public EvaluationContextConverter(LDLogger logger) { this.valueConverter = new ValueConverter(logger); } + /** + * Create an LDContext from an EvaluationContext. + *

+ * A context will always be created, but the created context may be invalid. + * Log messages will be written to indicate the source of the problem. + * + * @param evaluationContext The evaluation context to convert. + * @return An LDContext containing information from the evaluation context. + */ public LDContext toLdContext(EvaluationContext evaluationContext) { // Using the kind as a map here because getting a value from an immutable context that doesn't exist // throws. https://github.com/open-feature/java-sdk/pull/300 @@ -33,16 +41,16 @@ public LDContext toLdContext(EvaluationContext evaluationContext) { Value kindAsValue = attributes.get("kind"); String finalKind = "user"; - if(kindAsValue != null && kindAsValue.isString()) { + if (kindAsValue != null && kindAsValue.isString()) { String kindString = kindAsValue.asString(); - if(kindString == "multi") { + if (kindString == "multi") { // A multi-context. return BuildMultiContext(evaluationContext); } else { // Single context with specified kind. finalKind = kindString; } - } else if(kindAsValue != null) { + } else if (kindAsValue != null) { logger.error("The evaluation context contained an invalid kind."); } // No kind specified, so it is a user kind. @@ -57,42 +65,51 @@ public LDContext toLdContext(EvaluationContext evaluationContext) { /** * Get and validate a targeting key. + * * @param targetingKey Targeting key as a string, or null. - * @param keyAsValue Key as a Value, or null. + * @param keyAsValue Key as a Value, or null. * @return Returns a key, or null if one is not available. */ private String getTargetingKey(String targetingKey, Value keyAsValue) { // Currently the targeting key will always have a value, but it can be empty. // So we want to tread an empty string as a not defined one. Later it could // become null, so we will need to check that. - if(targetingKey != "" && keyAsValue != null && keyAsValue.isString()) { + if (targetingKey != "" && keyAsValue != null && keyAsValue.isString()) { // There is both a targeting key and a key. It will work, but probably // is not intentional. logger.warn("EvaluationContext contained both a 'key' and 'targetingKey'."); } - if(keyAsValue != null && !keyAsValue.isString()) { + if (keyAsValue != null && !keyAsValue.isString()) { logger.warn("A non-string 'key' attribute was provided."); } - // Targeting key takes precedence over key, because targeting key is in the spec. - targetingKey = targetingKey != "" ? targetingKey : keyAsValue.asString(); - if(targetingKey == null) { - logger.error("The EvaluationContext must contain either a 'targetingKey' or a 'key' and the type" + - "must be a string."); + if (keyAsValue != null && keyAsValue.isString()) { + // Targeting key takes precedence over key, because targeting key is in the spec. + targetingKey = targetingKey != "" ? targetingKey : keyAsValue.asString(); + } + + if (targetingKey == null || targetingKey == "") { + logger.error("The EvaluationContext must contain either a 'targetingKey' or a 'key' and the type " + "must be a string."); } return targetingKey; } + /** + * Build a multi-context from an evaluation context. + * + * @param evaluationContext The evaluation context containing multi-context information. + * @return The built context. + */ private LDContext BuildMultiContext(EvaluationContext evaluationContext) { ContextMultiBuilder multiBuilder = LDContext.multiBuilder(); evaluationContext.asMap().forEach((kind, attributes) -> { // Do not need to do anything for the kind key. - if(kind == "kind") return; + if (kind == "kind") return; - if(!attributes.isStructure()) { + if (!attributes.isStructure()) { // The attributes need to be a structure to be part of a multi-context. logger.warn("Top level attributes in a multi-kind context should be Structure types."); return; @@ -100,7 +117,8 @@ private LDContext BuildMultiContext(EvaluationContext evaluationContext) { Map attributesMap = attributes.asStructure().asMap(); Value keyAsValue = attributesMap.get("key"); - String targetingKey = attributesMap.get("targetingKey").toString(); + Value targetingKeyAsValue = attributesMap.get("targetingKey"); + String targetingKey = targetingKeyAsValue != null ? targetingKeyAsValue.asString() : ""; targetingKey = getTargetingKey(targetingKey, keyAsValue); LDContext singleContext = BuildSingleContext(attributesMap, kind, targetingKey); @@ -109,37 +127,51 @@ private LDContext BuildMultiContext(EvaluationContext evaluationContext) { return multiBuilder.build(); } + /** + * Build either a single context, or a part of a multi-context. + * + * @param attributes The attributes for the context to contain. + * @param kind The kind of the context being generated. + * @param key The key for the context. + * @return A LDContext which can be either a single context or a part of a multi-context. + */ private LDContext BuildSingleContext(Map attributes, String kind, String key) { ContextBuilder builder = LDContext.builder(ContextKind.of(kind), key); attributes.forEach((attrKey, attrValue) -> { // Key has been processed, so we can skip it. - if(attrKey == "key" || attrKey == "targetingKey") return; + if (attrKey == "key" || attrKey == "targetingKey") return; - if(attrKey == "privateAttributes") { + if (attrKey == "privateAttributes") { List valueList = attrValue.asList(); - if(valueList == null) { + if (valueList == null) { logger.error("A key of 'privateAttributes' in an evaluation context must have a list value."); return; } boolean allStrings = valueList.stream().allMatch(item -> item.isString()); - if(!allStrings) { + if (!allStrings) { logger.error("A key of 'privateAttributes' must be a list of only string values."); return; } - builder.privateAttributes( - valueList.stream() - .map(item -> item.asString()) - .collect(Collectors.toList()) - .toArray(new String[0])); + builder.privateAttributes(valueList.stream().map(item -> item.asString()).collect(Collectors.toList()).toArray(new String[0])); return; } - if(attrKey == "anonymous") { - // TODO: Anonymous stuff. + if (attrKey == "anonymous") { + if (!attrValue.isBoolean()) { + logger.error("The attribute 'anonymous' must be a boolean and it was not."); + } else { + builder.anonymous(attrValue.asBoolean()); + } + return; } - if(attrKey == "name") { - // TODO: Name stuff. + if (attrKey == "name") { + if (!attrValue.isString()) { + logger.error("The attribute 'name' must be a string and it was not."); + } else { + builder.name(attrValue.asString()); + } + return; } builder.set(attrKey, valueConverter.toLdValue(attrValue)); diff --git a/src/main/java/com/launchdarkly/openfeature/serverprovider/ValueConverter.java b/src/main/java/com/launchdarkly/openfeature/serverprovider/ValueConverter.java index 8e889c7..d0c6bfd 100644 --- a/src/main/java/com/launchdarkly/openfeature/serverprovider/ValueConverter.java +++ b/src/main/java/com/launchdarkly/openfeature/serverprovider/ValueConverter.java @@ -23,32 +23,32 @@ public ValueConverter(LDLogger logger) { } public LDValue toLdValue(Value value) { - if(value.isNull()) { + if (value.isNull()) { return LDValue.ofNull(); } - if(value.isBoolean()) { + if (value.isBoolean()) { return LDValue.of(value.asBoolean()); } - if(value.isNumber()) { + if (value.isNumber()) { return LDValue.of(value.asDouble()); } - if(value.isString()) { + if (value.isString()) { return LDValue.of(value.asString()); } - if(value.isInstant()) { + if (value.isInstant()) { DateTimeFormatter formatter = DateTimeFormatter .ISO_DATE_TIME .withZone(ZoneId.from(ZoneOffset.UTC)); return LDValue.of(formatter.format(value.asInstant())); } - if(value.isList()) { + if (value.isList()) { List asList = value.asList(); List asLdValues = asList.stream() .map(this::toLdValue) .collect(Collectors.toList()); return LDValue.arrayOf(asLdValues.toArray(new LDValue[0])); } - if(value.isStructure()) { + if (value.isStructure()) { ObjectBuilder objectBuilder = LDValue.buildObject(); Structure structure = value.asStructure(); structure.asMap().forEach((itemKey, itemValue) -> { diff --git a/src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAContextConverter.java b/src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAContextConverter.java index 865e67f..4690225 100644 --- a/src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAContextConverter.java +++ b/src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAContextConverter.java @@ -1,21 +1,32 @@ package com.launchdarkly.openfeature.serverprovider; +import com.launchdarkly.logging.LDLogLevel; import com.launchdarkly.logging.LDLogger; import com.launchdarkly.sdk.ContextKind; import com.launchdarkly.sdk.LDContext; import dev.openfeature.sdk.EvaluationContext; import dev.openfeature.sdk.ImmutableContext; +import dev.openfeature.sdk.ImmutableStructure; import dev.openfeature.sdk.Value; import org.junit.Test; +import java.util.ArrayList; import java.util.HashMap; import static org.junit.Assert.*; public class GivenAContextConverter { - EvaluationContextConverter evaluationContextConverter = new EvaluationContextConverter(LDLogger.none()); + TestLogger testLogger = new TestLogger(); + EvaluationContextConverter evaluationContextConverter = new EvaluationContextConverter( + LDLogger.withAdapter(testLogger, "test-logger") + ); - @Test public void itCanCreateAContextFromAKeyOnly() { + private TestLogger.TestChannel logs() { + return testLogger.getChannel("test-logger"); + } + + @Test + public void itCanCreateAContextFromAKeyOnly() { LDContext expectedContext = LDContext.builder("the-key").build(); LDContext converted = evaluationContextConverter.toLdContext(new ImmutableContext("the-key")); assertEquals(expectedContext, converted); @@ -24,16 +35,137 @@ public class GivenAContextConverter { attributes.put("key", new Value("the-key")); LDContext convertedKey = evaluationContextConverter.toLdContext(new ImmutableContext(attributes)); assertEquals(expectedContext, convertedKey); + + assertFalse(logs().containsAnyLogs()); } - @Test public void itCanCreateAContextFromAKeyAndKind() { + @Test + public void itCanCreateAContextFromAKeyAndKind() { LDContext expectedContext = LDContext.builder(ContextKind.of("organization"), "org-key").build(); + LDContext converted = evaluationContextConverter + .toLdContext(new ImmutableContext("org-key", new HashMap() {{ + put("kind", new Value("organization")); + }})); + + assertEquals(expectedContext, converted); + + assertFalse(logs().containsAnyLogs()); + } + + @Test + public void itLogsAnErrorWhenThereIsNoTargetingKey() { + evaluationContextConverter.toLdContext(new ImmutableContext()); + + assertTrue(logs().expectedMessageInLevel(LDLogLevel.ERROR, + "The EvaluationContext must contain either a 'targetingKey' or a 'key' and the type " + + "must be a string.")); + } + + @Test + public void itGivesTargetingKeyPrecedence() { + LDContext expectedContext = LDContext.builder("key-to-use").build(); + + HashMap attributes = new HashMap(); + attributes.put("key", new Value("key-not-to-use")); + + LDContext converted = evaluationContextConverter.toLdContext( + new ImmutableContext("key-to-use", attributes)); + + assertEquals(expectedContext, converted); + + assertTrue(logs().expectedMessageInLevel(LDLogLevel.WARN, + "EvaluationContext contained both a 'key' and 'targetingKey'.")); + } + + @Test + public void itHandlesAKeyOfIncorrectType() { HashMap attributes = new HashMap(); - attributes.put("kind", new Value("organization")); + attributes.put("key", new Value(0)); + + evaluationContextConverter.toLdContext( + new ImmutableContext(attributes)); + + assertTrue(logs().expectedMessageInLevel(LDLogLevel.WARN, + "A non-string 'key' attribute was provided.")); + + assertTrue(logs().expectedMessageInLevel(LDLogLevel.ERROR, + "The EvaluationContext must contain either a 'targetingKey' or a 'key' and the type " + + "must be a string.")); + } + + @Test + public void itHandlesInvalidBuiltInAttributes() { + LDContext expectedContext = LDContext.builder("user-key").build(); + + HashMap attributes = new HashMap(); + attributes.put("name", new Value(3)); + attributes.put("anonymous", new Value("potato")); + // The attributes were not valid, so they should be discarded. + LDContext converted = evaluationContextConverter + .toLdContext(new ImmutableContext("user-key", attributes)); + + assertEquals(expectedContext, converted); + + assertTrue(logs().containsAnyLogs()); + assertTrue(logs().expectedMessageInLevel(LDLogLevel.ERROR, + "The attribute 'name' must be a string and it was not.")); + assertTrue(logs().expectedMessageInLevel(LDLogLevel.ERROR, + "The attribute 'anonymous' must be a boolean and it was not.")); + assertEquals(2, logs().countForLevel(LDLogLevel.ERROR)); + + } + + @Test + public void itHandlesValidBuiltInAttributes() { + LDContext expectedContext = LDContext.builder("user-key") + .name("the-name") + .anonymous(true) + .build(); + + HashMap attributes = new HashMap(); + attributes.put("name", new Value("the-name")); + attributes.put("anonymous", new Value(true)); + LDContext converted = evaluationContextConverter - .toLdContext(new ImmutableContext("org-key", attributes)); + .toLdContext(new ImmutableContext("user-key", attributes)); assertEquals(expectedContext, converted); + assertFalse(logs().containsAnyLogs()); + } + + @Test + public void itCanCreateAValidMultiKindContext() { + LDContext expectedContext = LDContext.createMulti( + LDContext.builder(ContextKind.of("organization"), "my-org-key") + .name("the-org-name") + .set("myCustomAttribute", "myAttributeValue") + .privateAttributes(new String[]{"myCustomAttribute"}) + .build(), + LDContext.builder("my-user-key") + .anonymous(true) + .set("myCustomAttribute", "myAttributeValue") + .build() + ); + + EvaluationContext evaluationContext = new ImmutableContext(new HashMap() {{ + put("kind", new Value("multi")); + put("organization", new Value(new ImmutableStructure(new HashMap() {{ + put("name", new Value("the-org-name")); + put("targetingKey", new Value("my-org-key")); + put("myCustomAttribute", new Value("myAttributeValue")); + put("privateAttributes", new Value(new ArrayList() {{ + add(new Value("myCustomAttribute")); + }})); + }}))); + put("user", new Value(new ImmutableStructure(new HashMap() {{ + put("key", new Value("my-user-key")); + put("anonymous", new Value(true)); + put("myCustomAttribute", new Value("myAttributeValue")); + }}))); + }}); + + assertEquals(expectedContext, evaluationContextConverter.toLdContext(evaluationContext)); + assertFalse(logs().containsAnyLogs()); } } diff --git a/src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAValueConverter.java b/src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAValueConverter.java index ae28462..0014451 100644 --- a/src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAValueConverter.java +++ b/src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAValueConverter.java @@ -16,12 +16,14 @@ public class GivenAValueConverter { private ValueConverter valueConverter = new ValueConverter(LDLogger.none()); private final Double EPSILON = 0.00001; - @Test public void itCanConvertNull() { + @Test + public void itCanConvertNull() { LDValue value = valueConverter.toLdValue(new Value()); assertTrue(value.isNull()); } - @Test public void itCanConvertBooleans() { + @Test + public void itCanConvertBooleans() { LDValue trueValue = valueConverter.toLdValue(new Value(true)); assertTrue(trueValue.booleanValue()); assertEquals(trueValue.getType(), LDValueType.BOOLEAN); @@ -31,7 +33,8 @@ public class GivenAValueConverter { assertEquals(falseValue.getType(), LDValueType.BOOLEAN); } - @Test public void itCanConvertNumbers() { + @Test + public void itCanConvertNumbers() { LDValue zeroValue = valueConverter.toLdValue(new Value(0)); assertEquals(0.0, zeroValue.doubleValue(), EPSILON); assertTrue(zeroValue.isNumber()); @@ -41,19 +44,22 @@ public class GivenAValueConverter { assertTrue(numberValue.isNumber()); } - @Test public void itCanConvertStrings() { + @Test + public void itCanConvertStrings() { LDValue stringValue = valueConverter.toLdValue(new Value("the string")); assertTrue(stringValue.isString()); assertEquals("the string", stringValue.stringValue()); } - @Test public void itCanConvertInstants() { + @Test + public void itCanConvertInstants() { LDValue dateString = valueConverter.toLdValue(new Value(Instant.ofEpochMilli(0))); assertEquals("1970-01-01T00:00:00Z", dateString.stringValue()); } - @Test public void itCanConvertLists() { - Value ofValueList = new Value(new ArrayList(){{ + @Test + public void itCanConvertLists() { + Value ofValueList = new Value(new ArrayList() {{ add(new Value(true)); add(new Value(false)); add(new Value(17)); diff --git a/src/test/java/com/launchdarkly/openfeature/serverprovider/TestLogger.java b/src/test/java/com/launchdarkly/openfeature/serverprovider/TestLogger.java new file mode 100644 index 0000000..fddf8eb --- /dev/null +++ b/src/test/java/com/launchdarkly/openfeature/serverprovider/TestLogger.java @@ -0,0 +1,91 @@ +package com.launchdarkly.openfeature.serverprovider; + +import com.launchdarkly.logging.LDLogAdapter; +import com.launchdarkly.logging.LDLogLevel; +import jdk.nashorn.internal.runtime.regexp.joni.Regex; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * A log adapter that can be used in tests to capture log messages + * and validate the content of those messages. + */ +class TestLogger implements LDLogAdapter { + private HashMap channels = new HashMap<>(); + + public TestChannel getChannel(String name) { + return channels.get(name); + } + + public class TestChannel implements Channel { + private String name; + + private HashMap> messages = new HashMap(); + + public int countForLevel(LDLogLevel level) { + if (messages.containsKey(level)) { + return messages.get(level).size(); + } + return 0; + } + + public boolean expectedMessageInLevel(LDLogLevel level, String regexString) { + if (messages.containsKey(level)) { + return messages.get(level).stream().anyMatch(value -> { + return value.matches(regexString); + }); + } + return false; + } + + public boolean containsAnyLogs() { + return messages.size() != 0; + } + + private TestChannel(String name) { + this.name = name; + } + + private void addMessage(LDLogLevel ldLogLevel, String message) { + ArrayList forLevel = messages.getOrDefault(ldLogLevel, new ArrayList()); + + forLevel.add(message); + + // May already exist, but this makes the logic simpler. + messages.put(ldLogLevel, forLevel); + } + + @Override + public boolean isEnabled(LDLogLevel ldLogLevel) { + return true; + } + + @Override + public void log(LDLogLevel ldLogLevel, Object o) { + addMessage(ldLogLevel, o.toString()); + } + + @Override + public void log(LDLogLevel ldLogLevel, String s, Object o) { + addMessage(ldLogLevel, String.format(s, o)); + } + + @Override + public void log(LDLogLevel ldLogLevel, String s, Object o, Object o1) { + addMessage(ldLogLevel, String.format(s, o, o1)); + } + + @Override + public void log(LDLogLevel ldLogLevel, String s, Object... objects) { + addMessage(ldLogLevel, String.format(s, objects)); + } + } + + @Override + public Channel newChannel(String name) { + TestChannel newChannel = new TestChannel(name); + channels.put(name, newChannel); + return newChannel; + } +} From ab92c0707b234ff416b64d3fa5fe707510585931 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Thu, 16 Feb 2023 14:58:32 -0800 Subject: [PATCH 04/10] Add the ability to convert evaluation details. --- .../EvaluationDetailConverter.java | 100 +++++++++++ .../serverprovider/LDValueConverter.java | 64 +++++++ .../GivenAnEvaluationDetailConverter.java | 162 ++++++++++++++++++ 3 files changed, 326 insertions(+) create mode 100644 src/main/java/com/launchdarkly/openfeature/serverprovider/LDValueConverter.java create mode 100644 src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAnEvaluationDetailConverter.java diff --git a/src/main/java/com/launchdarkly/openfeature/serverprovider/EvaluationDetailConverter.java b/src/main/java/com/launchdarkly/openfeature/serverprovider/EvaluationDetailConverter.java index de3254c..19ba3a4 100644 --- a/src/main/java/com/launchdarkly/openfeature/serverprovider/EvaluationDetailConverter.java +++ b/src/main/java/com/launchdarkly/openfeature/serverprovider/EvaluationDetailConverter.java @@ -1,7 +1,107 @@ package com.launchdarkly.openfeature.serverprovider; +import com.launchdarkly.logging.LDLogger; +import com.launchdarkly.sdk.EvaluationDetail; +import com.launchdarkly.sdk.EvaluationReason; +import com.launchdarkly.sdk.LDValue; +import dev.openfeature.sdk.ErrorCode; +import dev.openfeature.sdk.ProviderEvaluation; +import dev.openfeature.sdk.Reason; +import dev.openfeature.sdk.Value; + /** * Converts an EvaluationDetail into an OpenFeature ResolutionDetails. */ class EvaluationDetailConverter { + LDLogger logger; + LDValueConverter ldValueConverter; + + public EvaluationDetailConverter(LDLogger logger) { + this.logger = logger; + this.ldValueConverter = new LDValueConverter(logger); + } + + /** + * This method can convert types other than Structures or Arrays. + * + * @param detail The detail to convert to a provider detail. + * @param The type of the evaluation result. + * @return The provider detail. + */ + public ProviderEvaluation toEvaluationDetails(EvaluationDetail detail) { + T value = detail.getValue(); + EvaluationReason reason = detail.getReason(); + boolean isDefault = detail.isDefaultValue(); + int variationIndex = detail.getVariationIndex(); + + return getProviderEvaluation(value, reason, isDefault, variationIndex); + } + + /** + * Convert Array and Structure type results. + * There are two different methods there isn't specialization, so there will need to be runtime decision + * about which method to call. This should be based on the method called in the provider interface. + * @param detail The detail to convert. + * @return The converted detail. + */ + public ProviderEvaluation toEvaluationDetailsLdValue(EvaluationDetail detail) { + Value value = ldValueConverter.toValue(detail.getValue()); + EvaluationReason reason = detail.getReason(); + boolean isDefault = detail.isDefaultValue(); + int variationIndex = detail.getVariationIndex(); + + return getProviderEvaluation(value, reason, isDefault, variationIndex); + } + + private static ProviderEvaluation getProviderEvaluation(T value, EvaluationReason reason, boolean isDefault, int variationIndex) { + ProviderEvaluation.ProviderEvaluationBuilder builder = ProviderEvaluation.builder() + .value(value) + .reason(KindToString(reason.getKind())); + if (reason.getKind() == EvaluationReason.Kind.ERROR) { + builder.errorCode(ErrorKindToErrorCode(reason.getErrorKind())); + } + if (!isDefault) { + builder.variant(String.valueOf(variationIndex)); + } + + return builder.build(); + } + + private static String KindToString(EvaluationReason.Kind kind) { + switch (kind) { + case OFF: + return Reason.DISABLED.toString(); + case TARGET_MATCH: + return Reason.TARGETING_MATCH.toString(); + case ERROR: + return Reason.ERROR.toString(); + case FALLTHROUGH: + // Intentional fallthrough + case RULE_MATCH: + // Intentional fallthrough + case PREREQUISITE_FAILED: + // Intentional fallthrough + default: + return kind.toString(); + } + } + + private static ErrorCode ErrorKindToErrorCode(EvaluationReason.ErrorKind errorKind) { + switch (errorKind) { + case CLIENT_NOT_READY: + return ErrorCode.PROVIDER_NOT_READY; + case FLAG_NOT_FOUND: + return ErrorCode.FLAG_NOT_FOUND; + case MALFORMED_FLAG: + return ErrorCode.PARSE_ERROR; + case USER_NOT_SPECIFIED: + return ErrorCode.TARGETING_KEY_MISSING; + case WRONG_TYPE: + return ErrorCode.TYPE_MISMATCH; + case EXCEPTION: + // Intentional fallthrough + default: + return ErrorCode.GENERAL; + } + } } diff --git a/src/main/java/com/launchdarkly/openfeature/serverprovider/LDValueConverter.java b/src/main/java/com/launchdarkly/openfeature/serverprovider/LDValueConverter.java new file mode 100644 index 0000000..9f13563 --- /dev/null +++ b/src/main/java/com/launchdarkly/openfeature/serverprovider/LDValueConverter.java @@ -0,0 +1,64 @@ +package com.launchdarkly.openfeature.serverprovider; + +import com.launchdarkly.logging.LDLogger; +import com.launchdarkly.sdk.ArrayBuilder; +import com.launchdarkly.sdk.LDValue; +import dev.openfeature.sdk.ImmutableStructure; +import dev.openfeature.sdk.Value; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +/** + * Provides methods for converting an LDValue into an OpenFeature Value. + */ +public class LDValueConverter { + private final LDLogger logger; + + public LDValueConverter(LDLogger logger) { + this.logger = logger; + } + + public Value toValue(LDValue value) { + switch(value.getType()) { + case NULL: + return new Value(); + case BOOLEAN: + return new Value(value.booleanValue()); + case NUMBER: + return new Value(value.doubleValue()); + case STRING: + return new Value(value.stringValue()); + case ARRAY: + return new Value(StreamSupport.stream(value.values().spliterator(), false) + .map(this::toValue) + .collect(Collectors.toList())); + case OBJECT: + List keys = new ArrayList(); + value.keys().forEach(keys::add); + + List values = new ArrayList(); + value.values().forEach(values::add); + + if(keys.size() != values.size()) { + logger.error("Could not get Object representation from LDValue. Returning a new Value(null)."); + return new Value(); + } + + HashMap converted = new HashMap(); + for (int keyIndex = 0; keyIndex < keys.size(); keyIndex++) { + String key = keys.get(keyIndex); + LDValue itemValue = values.get(keyIndex); + converted.put(key, toValue(itemValue)); + } + return new Value(new ImmutableStructure(converted)); + default: + logger.error("Unrecognized type converting result. Returning a new Value(null)."); + // Will only happen if new types are added. + return new Value(); + } + } +} diff --git a/src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAnEvaluationDetailConverter.java b/src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAnEvaluationDetailConverter.java new file mode 100644 index 0000000..121ffa1 --- /dev/null +++ b/src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAnEvaluationDetailConverter.java @@ -0,0 +1,162 @@ +package com.launchdarkly.openfeature.serverprovider; + +import com.launchdarkly.logging.LDLogger; +import com.launchdarkly.sdk.*; +import dev.openfeature.sdk.*; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.*; + +public class GivenAnEvaluationDetailConverter { + private final Double EPSILON = 0.00001; + TestLogger testLogger = new TestLogger(); + EvaluationDetailConverter evaluationDetailConverter = new EvaluationDetailConverter( + LDLogger.withAdapter(testLogger, "test-logger") + ); + + private TestLogger.TestChannel logs() { + return testLogger.getChannel("test-logger"); + } + + @Test + public void itCanConvertDoubleEvaluationDetail() { + EvaluationDetail inputDetail = EvaluationDetail.fromValue( + 3.0, 17, EvaluationReason.off()); + + ProviderEvaluation converted = evaluationDetailConverter.toEvaluationDetails(inputDetail); + + assertEquals(3.0, converted.getValue().doubleValue(), EPSILON); + assertEquals("17", converted.getVariant()); + assertEquals(Reason.DISABLED.toString(), converted.getReason()); + } + + @Test + public void itCanConvertAStringEvaluationDetail() { + EvaluationDetail inputDetail = EvaluationDetail.fromValue( + "the-value", 12, EvaluationReason.off()); + + ProviderEvaluation converted = evaluationDetailConverter.toEvaluationDetails(inputDetail); + + assertEquals("the-value", converted.getValue()); + assertEquals("12", converted.getVariant()); + assertEquals(Reason.DISABLED.toString(), converted.getReason()); + } + + @Test + public void itCanHandleDifferentReasons() { + EvaluationDetail off = EvaluationDetail.fromValue(true, 0, EvaluationReason.off()); + + assertEquals(Reason.DISABLED.toString(), evaluationDetailConverter.toEvaluationDetails(off).getReason()); + + EvaluationDetail targetMatch = EvaluationDetail.fromValue( + true, 0, EvaluationReason.targetMatch()); + + assertEquals(Reason.TARGETING_MATCH.toString(), evaluationDetailConverter.toEvaluationDetails(targetMatch).getReason()); + + EvaluationDetail fallthrough = EvaluationDetail.fromValue( + true, 0, EvaluationReason.fallthrough()); + + assertEquals("FALLTHROUGH", evaluationDetailConverter.toEvaluationDetails(fallthrough).getReason()); + + EvaluationDetail ruleMatch = EvaluationDetail.fromValue( + true, 0, EvaluationReason.ruleMatch(0, "")); + + assertEquals("RULE_MATCH", evaluationDetailConverter.toEvaluationDetails(ruleMatch).getReason()); + + EvaluationDetail prereqFailed = EvaluationDetail.fromValue( + true, 0, EvaluationReason.prerequisiteFailed("the-key")); + + assertEquals("PREREQUISITE_FAILED", evaluationDetailConverter.toEvaluationDetails(prereqFailed).getReason()); + } + + @Test + public void itCanHandleDifferentErrors() { + EvaluationDetail clientNotReady = EvaluationDetail.fromValue( + true, 0, EvaluationReason.error(EvaluationReason.ErrorKind.CLIENT_NOT_READY)); + + assertEquals(ErrorCode.PROVIDER_NOT_READY, + evaluationDetailConverter.toEvaluationDetails(clientNotReady).getErrorCode()); + + EvaluationDetail flagNotFound = EvaluationDetail.fromValue( + true, 0, EvaluationReason.error(EvaluationReason.ErrorKind.FLAG_NOT_FOUND)); + + assertEquals(ErrorCode.FLAG_NOT_FOUND, + evaluationDetailConverter.toEvaluationDetails(flagNotFound).getErrorCode()); + + EvaluationDetail malformedFlag = EvaluationDetail.fromValue( + true, 0, EvaluationReason.error(EvaluationReason.ErrorKind.MALFORMED_FLAG)); + + assertEquals(ErrorCode.PARSE_ERROR, + evaluationDetailConverter.toEvaluationDetails(malformedFlag).getErrorCode()); + + EvaluationDetail userNotSpecified = EvaluationDetail.fromValue( + true, 0, EvaluationReason.error(EvaluationReason.ErrorKind.USER_NOT_SPECIFIED)); + + assertEquals(ErrorCode.TARGETING_KEY_MISSING, + evaluationDetailConverter.toEvaluationDetails(userNotSpecified).getErrorCode()); + + EvaluationDetail wrongType = EvaluationDetail.fromValue( + true, 0, EvaluationReason.error(EvaluationReason.ErrorKind.WRONG_TYPE)); + + assertEquals(ErrorCode.TYPE_MISMATCH, + evaluationDetailConverter.toEvaluationDetails(wrongType).getErrorCode()); + + EvaluationDetail exception = EvaluationDetail.fromValue( + true, 0, EvaluationReason.error(EvaluationReason.ErrorKind.EXCEPTION)); + + assertEquals(ErrorCode.GENERAL, + evaluationDetailConverter.toEvaluationDetails(exception).getErrorCode()); + } + + @Test + public void itCanHandleAnArrayResult() { + ArrayBuilder arrayBuilder = new ArrayBuilder(); + arrayBuilder.add(1.2); + arrayBuilder.add("potato"); + arrayBuilder.add(new ArrayBuilder().add(17.0).build()); + arrayBuilder.add(new ObjectBuilder().put("aKey", "aValue").build()); + + EvaluationDetail arrayDetail = EvaluationDetail.fromValue( + arrayBuilder.build(), 0, EvaluationReason.off()); + + ProviderEvaluation converted = evaluationDetailConverter.toEvaluationDetailsLdValue(arrayDetail); + + List convertedValue = converted.getValue().asList(); + + assertEquals(1.2, convertedValue.get(0).asDouble(), EPSILON); + assertEquals("potato", convertedValue.get(1).asString()); + + List nested = convertedValue.get(2).asList(); + + assertEquals(17.0, nested.get(0).asDouble(), EPSILON); + + Structure nestedStructure = convertedValue.get(3).asStructure(); + assertEquals("aValue", nestedStructure.getValue("aKey").asString()); + } + + @Test + public void itCanHandleAnObjectResult() { + ObjectBuilder objectBuilder = new ObjectBuilder(); + objectBuilder.put("aKey", "aValue"); + objectBuilder.put("objectKey", new ObjectBuilder().put("bKey", "bValue").build()); + objectBuilder.put("arrayKey", new ArrayBuilder().add(17.0).build()); + + EvaluationDetail objectDetail = EvaluationDetail.fromValue( + objectBuilder.build(), 0, EvaluationReason.off()); + + ProviderEvaluation converted = evaluationDetailConverter.toEvaluationDetailsLdValue(objectDetail); + + Structure convertedStructure = converted.getValue().asStructure(); + + assertEquals("aValue", convertedStructure.getValue("aKey").asString()); + + Structure nestedStructure = convertedStructure.getValue("objectKey").asStructure(); + assertEquals("bValue", nestedStructure.getValue("bKey").asString()); + + List nestedList = convertedStructure.getValue("arrayKey").asList(); + + assertEquals(17.0, nestedList.get(0).asDouble(), EPSILON); + } +} From b9523c3124013d2cfab8aea4076e18f0a8f1ebe3 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Thu, 16 Feb 2023 15:15:40 -0800 Subject: [PATCH 05/10] Add more value conversion tests. --- .../serverprovider/GivenAValueConverter.java | 28 ++++++ .../GivenAnLdValueConverter.java | 99 +++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAnLdValueConverter.java diff --git a/src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAValueConverter.java b/src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAValueConverter.java index 0014451..ab1a751 100644 --- a/src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAValueConverter.java +++ b/src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAValueConverter.java @@ -3,11 +3,13 @@ import com.launchdarkly.logging.LDLogger; import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.LDValueType; +import dev.openfeature.sdk.ImmutableStructure; import dev.openfeature.sdk.Value; import org.junit.Test; import java.time.Instant; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import static org.junit.Assert.*; @@ -79,4 +81,30 @@ public void itCanConvertLists() { assertEquals(42.5, ldValueList.get(3).doubleValue(), EPSILON); assertEquals("string", ldValueList.get(4).stringValue()); } + + @Test + public void itCanConvertStructures() { + Value ofValueStructure = new Value(new ImmutableStructure(new HashMap(){{ + put("aKey", new Value("aValue")); + put("structKey", new Value(new ImmutableStructure(new HashMap(){{ + put("bKey", new Value("bValue")); + }}))); + }})); + + LDValue ldValue = valueConverter.toLdValue(ofValueStructure); + + List keyList = new ArrayList(); + ldValue.keys().forEach(keyList::add); + + List valueList = new ArrayList(); + ldValue.values().forEach(valueList::add); + + assertEquals("aKey", keyList.get(0)); + assertEquals("structKey", keyList.get(1)); + + assertEquals("aValue", valueList.get(0).stringValue()); + + assertEquals("bKey", valueList.get(1).keys().iterator().next()); + assertEquals("bValue", valueList.get(1).values().iterator().next().stringValue()); + } } diff --git a/src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAnLdValueConverter.java b/src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAnLdValueConverter.java new file mode 100644 index 0000000..2222d4f --- /dev/null +++ b/src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAnLdValueConverter.java @@ -0,0 +1,99 @@ +package com.launchdarkly.openfeature.serverprovider; + +import com.launchdarkly.logging.LDLogger; +import com.launchdarkly.sdk.ArrayBuilder; +import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.ObjectBuilder; +import dev.openfeature.sdk.ImmutableStructure; +import dev.openfeature.sdk.Structure; +import dev.openfeature.sdk.Value; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import static org.junit.Assert.*; + +public class GivenAnLdValueConverter { + private LDValueConverter valueConverter = new LDValueConverter(LDLogger.none()); + private final Double EPSILON = 0.00001; + + @Test + public void itCanConvertNull() { + Value value = valueConverter.toValue(LDValue.ofNull()); + assertTrue(value.isNull()); + } + + @Test + public void itCanConvertBooleans() { + Value trueValue = valueConverter.toValue(LDValue.of(true)); + assertTrue(trueValue.asBoolean()); + assertTrue(trueValue.isBoolean()); + + Value falseValue = valueConverter.toValue(LDValue.of(false)); + assertFalse(falseValue.asBoolean()); + assertTrue(falseValue.isBoolean()); + } + + @Test + public void itCanConvertNumbers() { + Value zeroValue = valueConverter.toValue(LDValue.of(0.0)); + assertEquals(0.0, zeroValue.asDouble(), EPSILON); + assertTrue(zeroValue.isNumber()); + + Value numberValue = valueConverter.toValue(LDValue.of(1000.0)); + assertEquals(numberValue.asDouble(), 1000.0, EPSILON); + assertTrue(numberValue.isNumber()); + } + + @Test + public void itCanConvertStrings() { + Value stringValue = valueConverter.toValue(LDValue.of("the string")); + assertTrue(stringValue.isString()); + assertEquals("the string", stringValue.asString()); + } + + @Test + public void itCanConvertLists() { + LDValue ldValueList = new ArrayBuilder() + .add(true) + .add(false) + .add(17.0) + .add(42.5) + .add("string").build(); + + Value ofValue = valueConverter.toValue(ldValueList); + List ofValueList = ofValue.asList(); + + assertEquals(5, ofValueList.size()); + + assertTrue(ofValueList.get(0).asBoolean()); + assertFalse(ofValueList.get(1).asBoolean()); + assertEquals(17.0, ofValueList.get(2).asDouble(), EPSILON); + assertEquals(42.5, ofValueList.get(3).asDouble(), EPSILON); + assertEquals("string", ofValueList.get(4).asString()); + } + + @Test + public void itCanConvertStructures() { + Value ofValueStructure = new Value(new ImmutableStructure(new HashMap(){{ + put("aKey", new Value("aValue")); + put("structKey", new Value(new ImmutableStructure(new HashMap(){{ + put("bKey", new Value("bValue")); + }}))); + }})); + + LDValue ldStructValue = new ObjectBuilder() + .put("aKey", "aValue") + .put("structKey", new ObjectBuilder().put("bKey", "bValue").build()).build(); + + Value ofValue = valueConverter.toValue(ldStructValue); + + Structure ofStructure = ofValue.asStructure(); + assertEquals("aValue", ofStructure.getValue("aKey").asString()); + + Structure nested = ofStructure.getValue("structKey").asStructure(); + assertEquals("bValue", nested.getValue("bKey").asString()); + } +} From be7e734a11945362c9ce68f619fe284ec649556b Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Thu, 16 Feb 2023 15:47:43 -0800 Subject: [PATCH 06/10] Implement provider. --- .../openfeature/serverprovider/Provider.java | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java diff --git a/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java b/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java new file mode 100644 index 0000000..8ada47e --- /dev/null +++ b/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java @@ -0,0 +1,82 @@ +package com.launchdarkly.openfeature.serverprovider; + +import com.launchdarkly.logging.LDLogger; +import com.launchdarkly.sdk.EvaluationDetail; +import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.server.LDClient; +import dev.openfeature.sdk.*; + + +public class Provider implements FeatureProvider { + private class ProviderMetaData implements Metadata { + @Override + public String getName() { + return "LaunchDarkly.OpenFeature.ServerProvider"; + } + } + + private final Metadata metaData = new ProviderMetaData(); + + private LDLogger logger; + private EvaluationDetailConverter evaluationDetailConverter; + private ValueConverter valueConverter; + private EvaluationContextConverter evaluationContextConverter; + + private LDClient client; + + public Provider(LDClient client) { + this.client = client; + logger = LDLogger.none(); // TODO: Implement config. + + evaluationContextConverter = new EvaluationContextConverter(logger); + evaluationDetailConverter = new EvaluationDetailConverter(logger); + valueConverter = new ValueConverter(logger); + } + + + @Override + public Metadata getMetadata() { + return metaData; + } + + @Override + public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) { + EvaluationDetail detail + = this.client.boolVariationDetail(key, evaluationContextConverter.toLdContext(ctx), defaultValue); + + return evaluationDetailConverter.toEvaluationDetails(detail); + } + + @Override + public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) { + EvaluationDetail detail + = this.client.stringVariationDetail(key, evaluationContextConverter.toLdContext(ctx), defaultValue); + + return evaluationDetailConverter.toEvaluationDetails(detail); + } + + @Override + public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) { + EvaluationDetail detail + = this.client.intVariationDetail(key, evaluationContextConverter.toLdContext(ctx), defaultValue); + + return evaluationDetailConverter.toEvaluationDetails(detail); + } + + @Override + public ProviderEvaluation getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) { + EvaluationDetail detail + = this.client.doubleVariationDetail(key, evaluationContextConverter.toLdContext(ctx), defaultValue); + + return evaluationDetailConverter.toEvaluationDetails(detail); + } + + @Override + public ProviderEvaluation getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) { + EvaluationDetail detail + = this.client.jsonValueVariationDetail( + key, evaluationContextConverter.toLdContext(ctx), valueConverter.toLdValue(defaultValue)); + + return evaluationDetailConverter.toEvaluationDetailsLdValue(detail); + } +} From c5154c21aa9a17e4756aa63801cf1d6fa5c4ee2e Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Thu, 16 Feb 2023 16:35:56 -0800 Subject: [PATCH 07/10] Implement configuration and add some additional documentation to the provider. --- README.md | 17 +++- .../openfeature/serverprovider/Provider.java | 54 +++++++++++-- .../serverprovider/ProviderConfiguration.java | 79 +++++++++++++++++++ 3 files changed, 144 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/launchdarkly/openfeature/serverprovider/ProviderConfiguration.java diff --git a/README.md b/README.md index f43294b..6eabb77 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,22 @@ TODO: Implement ### Usage -TODO: Implement +```java +package org.example; + +import dev.openfeature.sdk.OpenFeatureAPI; +import com.launchdarkly.sdk.server.LDClient; + +public class Main { + public static void main(String[] args) { + LDClient ldClient = new LDClient("my-sdk-key"); + OpenFeatureAPI.getInstance().setProvider(new Provider(ldClient)); + + // Refer to OpenFeature documentation for getting a client and performing evaluations. + } +} + +``` Refer to the [SDK reference guide](https://docs.launchdarkly.com/sdk/server-side/java) for instructions on getting started with using the SDK. diff --git a/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java b/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java index 8ada47e..3650d41 100644 --- a/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java +++ b/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java @@ -1,14 +1,32 @@ package com.launchdarkly.openfeature.serverprovider; +import com.launchdarkly.logging.LDLogAdapter; import com.launchdarkly.logging.LDLogger; import com.launchdarkly.sdk.EvaluationDetail; import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.server.LDClient; +import com.launchdarkly.sdk.server.subsystems.LoggingConfiguration; import dev.openfeature.sdk.*; - +/** + * An OpenFeature {@link FeatureProvider} which enables the use of the LaunchDarkly Server-Side SDK for Java + * with OpenFeature. + *


+ *import dev.openfeature.sdk.OpenFeatureAPI;
+ *import com.launchdarkly.sdk.server.LDClient;
+ *
+ *public class Main {
+ *  public static void main(String[] args) {
+ *    LDClient ldClient = new LDClient("my-sdk-key");
+ *    OpenFeatureAPI.getInstance().setProvider(new Provider(ldClient));
+ *
+ *    // Refer to OpenFeature documentation for getting a client and performing evaluations.
+ *  }
+ *}
+ * 
+ */ public class Provider implements FeatureProvider { - private class ProviderMetaData implements Metadata { + private static final class ProviderMetaData implements Metadata { @Override public String getName() { return "LaunchDarkly.OpenFeature.ServerProvider"; @@ -24,15 +42,41 @@ public String getName() { private LDClient client; - public Provider(LDClient client) { + /** + * Create a provider with the given LaunchDarkly client and provider configuration. + *

+     * // Using the provider with a custom log level.
+     * new Provider(ldclient, ProviderConfiguration
+     *     .builder()
+     *     .logging(Components.logging().level(LDLogLevel.INFO)
+     *     .build());
+     * 
+ * + * @param client A {@link LDClient} instance. + * @param config Configuration for the provider. + */ + public Provider(LDClient client, ProviderConfiguration config) { this.client = client; - logger = LDLogger.none(); // TODO: Implement config. + LoggingConfiguration loggingConfig = config.getLoggingConfigurationFactory().build(null); + LDLogAdapter adapter = loggingConfig.getLogAdapter(); + logger = LDLogger.withAdapter(adapter, loggingConfig.getBaseLoggerName()); evaluationContextConverter = new EvaluationContextConverter(logger); evaluationDetailConverter = new EvaluationDetailConverter(logger); valueConverter = new ValueConverter(logger); } + /** + * Create a provider with the given LaunchDarkly client. + *

+ * The provider will be created with default configuration. + * + * @param client A {@link LDClient} instance. + */ + public Provider(LDClient client) { + this(client, ProviderConfiguration.builder().build()); + } + @Override public Metadata getMetadata() { @@ -75,7 +119,7 @@ public ProviderEvaluation getDoubleEvaluation(String key, Double default public ProviderEvaluation getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) { EvaluationDetail detail = this.client.jsonValueVariationDetail( - key, evaluationContextConverter.toLdContext(ctx), valueConverter.toLdValue(defaultValue)); + key, evaluationContextConverter.toLdContext(ctx), valueConverter.toLdValue(defaultValue)); return evaluationDetailConverter.toEvaluationDetailsLdValue(detail); } diff --git a/src/main/java/com/launchdarkly/openfeature/serverprovider/ProviderConfiguration.java b/src/main/java/com/launchdarkly/openfeature/serverprovider/ProviderConfiguration.java new file mode 100644 index 0000000..b233bae --- /dev/null +++ b/src/main/java/com/launchdarkly/openfeature/serverprovider/ProviderConfiguration.java @@ -0,0 +1,79 @@ +package com.launchdarkly.openfeature.serverprovider; + +import com.launchdarkly.logging.LDLogAdapter; +import com.launchdarkly.sdk.server.Components; +import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.server.subsystems.LoggingConfiguration; + +/** + * An immutable configuration for the provider. Must be created using a {@link ProviderConfigurationBuilder}. + *


+ *     ProviderConfiguration.builder().build();
+ * 
+ */ +public final class ProviderConfiguration { + ProviderConfigurationBuilder builder; + + private ProviderConfiguration(ProviderConfigurationBuilder builder) { + this.builder = builder; + } + + /** + * A mutable object that uses the Builder pattern to specify properties for a {@link ProviderConfiguration} object. + */ + public static final class ProviderConfigurationBuilder { + private ComponentConfigurer loggingConfigurer; + + /** + * Build a provider configuration. + * + * @return And immutable provider configuration. + */ + public ProviderConfiguration build() { + if (this.loggingConfigurer == null) { + this.loggingConfigurer = Components.logging(); + } + return new ProviderConfiguration(this); + } + + /** + * Assign an existing logging configuration. + * + * @param config The logging configuration to use. + * @return This builder. + */ + public ProviderConfigurationBuilder logging(ComponentConfigurer config) { + this.loggingConfigurer = config; + return this; + } + + /** + * Create a logging configuration based on an {@link LDLogAdapter}. + * + * @param logAdapter The log adapter to use. + * @return This builder. + */ + public ProviderConfigurationBuilder logging(LDLogAdapter logAdapter) { + this.loggingConfigurer = Components.logging(logAdapter); + return this; + } + } + + /** + * Get a new builder instance. + * + * @return A provider configuration builder. + */ + public static ProviderConfigurationBuilder builder() { + return new ProviderConfigurationBuilder(); + } + + /** + * Get the logging factory to generate logging configuration. + * + * @return A logging factory. + */ + public ComponentConfigurer getLoggingConfigurationFactory() { + return builder.loggingConfigurer; + } +} From f323d6ce3b4cbadda7101ae5cd57b2e9607a88b9 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 17 Feb 2023 09:17:04 -0800 Subject: [PATCH 08/10] Implement testing for ProviderConfiguration. --- .../openfeature/serverprovider/Provider.java | 1 - .../ProviderConfigurationTests.java | 53 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/launchdarkly/openfeature/serverprovider/ProviderConfigurationTests.java diff --git a/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java b/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java index 3650d41..ac74438 100644 --- a/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java +++ b/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java @@ -77,7 +77,6 @@ public Provider(LDClient client) { this(client, ProviderConfiguration.builder().build()); } - @Override public Metadata getMetadata() { return metaData; diff --git a/src/test/java/com/launchdarkly/openfeature/serverprovider/ProviderConfigurationTests.java b/src/test/java/com/launchdarkly/openfeature/serverprovider/ProviderConfigurationTests.java new file mode 100644 index 0000000..46f71b0 --- /dev/null +++ b/src/test/java/com/launchdarkly/openfeature/serverprovider/ProviderConfigurationTests.java @@ -0,0 +1,53 @@ +package com.launchdarkly.openfeature.serverprovider; + +import com.launchdarkly.logging.LDLogAdapter; +import com.launchdarkly.logging.LDLogLevel; +import com.launchdarkly.logging.LDLogger; +import com.launchdarkly.sdk.server.Components; +import com.launchdarkly.sdk.server.subsystems.LoggingConfiguration; +import org.junit.Test; + +import static org.junit.Assert.*; + + +public class ProviderConfigurationTests { + @Test + public void itCanBuildADefaultConfiguration() { + ProviderConfiguration defaultConfig = ProviderConfiguration.builder().build(); + assertNotNull(defaultConfig.getLoggingConfigurationFactory()); + } + + @Test + public void itCanBeUsedWithALogAdapter() { + TestLogger logAdapter = new TestLogger(); + ProviderConfiguration withLogAdapter = ProviderConfiguration.builder() + .logging(logAdapter).build(); + + LoggingConfiguration loggingConfig = withLogAdapter.getLoggingConfigurationFactory() + .build(null); + + LDLogger logger = LDLogger.withAdapter(loggingConfig.getLogAdapter(), "the-name"); + logger.error("this is the error"); + + assertTrue(logAdapter + .getChannel("the-name") + .expectedMessageInLevel(LDLogLevel.ERROR, "this is the error")); + } + + @Test + public void itCanBeUsedWithALoggingComponentConfigurer() { + TestLogger logAdapter = new TestLogger(); + ProviderConfiguration withConfigurer = ProviderConfiguration.builder() + .logging(Components.logging().adapter(logAdapter)).build(); + + LoggingConfiguration loggingConfig = withConfigurer.getLoggingConfigurationFactory() + .build(null); + + LDLogger logger = LDLogger.withAdapter(loggingConfig.getLogAdapter(), "the-name"); + logger.error("this is the error"); + + assertTrue(logAdapter + .getChannel("the-name") + .expectedMessageInLevel(LDLogLevel.ERROR, "this is the error")); + } +} From 52ca4297a321e36f20e1bee588bc36671994a3c9 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 17 Feb 2023 11:17:40 -0800 Subject: [PATCH 09/10] Implement provider tests. --- build.gradle | 1 + .../openfeature/serverprovider/Provider.java | 7 +- .../GivenAProviderWithMockLdClient.java | 124 ++++++++++++++++++ 3 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAProviderWithMockLdClient.java diff --git a/build.gradle b/build.gradle index b1248be..2bf0355 100644 --- a/build.gradle +++ b/build.gradle @@ -28,5 +28,6 @@ dependencies { // Use JUnit test framework testImplementation 'junit:junit:4.12' + testImplementation "org.mockito:mockito-core:3.+" } diff --git a/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java b/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java index ac74438..8304100 100644 --- a/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java +++ b/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java @@ -5,6 +5,7 @@ import com.launchdarkly.sdk.EvaluationDetail; import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.server.LDClient; +import com.launchdarkly.sdk.server.interfaces.LDClientInterface; import com.launchdarkly.sdk.server.subsystems.LoggingConfiguration; import dev.openfeature.sdk.*; @@ -40,7 +41,7 @@ public String getName() { private ValueConverter valueConverter; private EvaluationContextConverter evaluationContextConverter; - private LDClient client; + private LDClientInterface client; /** * Create a provider with the given LaunchDarkly client and provider configuration. @@ -55,7 +56,7 @@ public String getName() { * @param client A {@link LDClient} instance. * @param config Configuration for the provider. */ - public Provider(LDClient client, ProviderConfiguration config) { + public Provider(LDClientInterface client, ProviderConfiguration config) { this.client = client; LoggingConfiguration loggingConfig = config.getLoggingConfigurationFactory().build(null); LDLogAdapter adapter = loggingConfig.getLogAdapter(); @@ -73,7 +74,7 @@ public Provider(LDClient client, ProviderConfiguration config) { * * @param client A {@link LDClient} instance. */ - public Provider(LDClient client) { + public Provider(LDClientInterface client) { this(client, ProviderConfiguration.builder().build()); } diff --git a/src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAProviderWithMockLdClient.java b/src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAProviderWithMockLdClient.java new file mode 100644 index 0000000..c4dc6f6 --- /dev/null +++ b/src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAProviderWithMockLdClient.java @@ -0,0 +1,124 @@ +package com.launchdarkly.openfeature.serverprovider; + +import com.launchdarkly.sdk.EvaluationDetail; +import com.launchdarkly.sdk.EvaluationReason; +import com.launchdarkly.sdk.LDContext; +import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.server.interfaces.LDClientInterface; +import dev.openfeature.sdk.*; +import org.junit.Test; + +import static org.mockito.Mockito.*; +import static org.junit.Assert.*; + +public class GivenAProviderWithMockLdClient { + LDClientInterface mockedLdClient = mock(LDClientInterface.class); + Provider ldProvider = new Provider(mockedLdClient); + + @Test + public void itCanProvideMetadata() { + Metadata metaData = ldProvider.getMetadata(); + assertEquals("LaunchDarkly.OpenFeature.ServerProvider", metaData.getName()); + } + + @Test + public void itCanDoABooleanEvaluation() { + EvaluationContext evaluationContext = new ImmutableContext("user-key"); + + when(mockedLdClient.boolVariationDetail("the-key", LDContext.create("user-key"), false)) + .thenReturn(EvaluationDetail.fromValue(true, 12, EvaluationReason.fallthrough())); + + OpenFeatureAPI.getInstance().setProvider(ldProvider); + + assertTrue(OpenFeatureAPI + .getInstance() + .getClient() + .getBooleanValue("the-key", false, evaluationContext)); + + FlagEvaluationDetails detailed = OpenFeatureAPI + .getInstance() + .getClient() + .getBooleanDetails("the-key", false, evaluationContext); + + assertEquals(true, detailed.getValue()); + assertEquals("12", detailed.getVariant()); + assertEquals("FALLTHROUGH", detailed.getReason()); + } + + @Test + public void itCanDoAStringEvaluation() { + EvaluationContext evaluationContext = new ImmutableContext("user-key"); + + when(mockedLdClient.stringVariationDetail("the-key", LDContext.create("user-key"), "default")) + .thenReturn(EvaluationDetail + .fromValue("evaluated", 17, EvaluationReason.off())); + + OpenFeatureAPI.getInstance().setProvider(ldProvider); + + assertEquals("evaluated", OpenFeatureAPI + .getInstance() + .getClient() + .getStringValue("the-key", "default", evaluationContext)); + + FlagEvaluationDetails detailed = OpenFeatureAPI + .getInstance() + .getClient() + .getStringDetails("the-key", "default", evaluationContext); + + assertEquals("evaluated", detailed.getValue()); + assertEquals("17", detailed.getVariant()); + assertEquals("DISABLED", detailed.getReason()); + } + + @Test + public void itCanDoADoubleEvaluation() { + EvaluationContext evaluationContext = new ImmutableContext("user-key"); + + when(mockedLdClient.doubleVariationDetail("the-key", LDContext.create("user-key"), 0.0)) + .thenReturn(EvaluationDetail.fromValue(1.0, 42, EvaluationReason.targetMatch())); + + OpenFeatureAPI.getInstance().setProvider(ldProvider); + assertEquals(1.0, OpenFeatureAPI + .getInstance() + .getClient() + .getDoubleValue("the-key", 0.0, evaluationContext), 0.00001); + + FlagEvaluationDetails detailed = OpenFeatureAPI + .getInstance() + .getClient() + .getDoubleDetails("the-key", 0.0, evaluationContext); + + assertEquals(1.0, detailed.getValue(), 0.00001); + assertEquals("42", detailed.getVariant()); + assertEquals("TARGETING_MATCH", detailed.getReason()); + } + + @Test + public void itCanDoAValueEvaluation() { + EvaluationContext evaluationContext = new ImmutableContext("user-key"); + + EvaluationDetail evaluationDetail = EvaluationDetail + .fromValue(LDValue.buildObject().put("aKey", "aValue").build(), 84, EvaluationReason.targetMatch()); + + when(mockedLdClient.jsonValueVariationDetail("the-key", LDContext.create("user-key"), LDValue.ofNull())) + .thenReturn(evaluationDetail); + + OpenFeatureAPI.getInstance().setProvider(ldProvider); + Value ofValue = OpenFeatureAPI + .getInstance() + .getClient() + .getObjectValue("the-key", new Value(), evaluationContext); + + assertEquals("aValue", ofValue.asStructure().getValue("aKey").asString()); + + FlagEvaluationDetails detailed = OpenFeatureAPI + .getInstance() + .getClient() + .getObjectDetails("the-key", new Value(), evaluationContext); + + assertEquals("aValue", detailed.getValue().asStructure().getValue("aKey").asString()); + + assertEquals("84", detailed.getVariant()); + assertEquals("TARGETING_MATCH", detailed.getReason()); + } +} From 7ba71ca204d3db28b6cc9643afaad2214fba7e2a Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 17 Feb 2023 11:33:38 -0800 Subject: [PATCH 10/10] Apply all IDE recommendations. --- .../openfeature/serverprovider/Provider.java | 10 +++++----- ...nverter.java => ContextConverterTest.java} | 20 +++++++++---------- ...ava => EvaluationDetailConverterTest.java} | 8 ++------ ...nverter.java => LdValueConverterTest.java} | 14 ++----------- .../ProviderConfigurationTests.java | 4 ++-- ...ithMockLdClient.java => ProviderTest.java} | 2 +- .../serverprovider/TestLogger.java | 17 ++++++---------- ...Converter.java => ValueConverterTest.java} | 10 +++++----- 8 files changed, 33 insertions(+), 52 deletions(-) rename src/test/java/com/launchdarkly/openfeature/serverprovider/{GivenAContextConverter.java => ContextConverterTest.java} (93%) rename src/test/java/com/launchdarkly/openfeature/serverprovider/{GivenAnEvaluationDetailConverter.java => EvaluationDetailConverterTest.java} (96%) rename src/test/java/com/launchdarkly/openfeature/serverprovider/{GivenAnLdValueConverter.java => LdValueConverterTest.java} (84%) rename src/test/java/com/launchdarkly/openfeature/serverprovider/{GivenAProviderWithMockLdClient.java => ProviderTest.java} (99%) rename src/test/java/com/launchdarkly/openfeature/serverprovider/{GivenAValueConverter.java => ValueConverterTest.java} (92%) diff --git a/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java b/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java index 8304100..9d6cf53 100644 --- a/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java +++ b/src/main/java/com/launchdarkly/openfeature/serverprovider/Provider.java @@ -36,12 +36,12 @@ public String getName() { private final Metadata metaData = new ProviderMetaData(); - private LDLogger logger; - private EvaluationDetailConverter evaluationDetailConverter; - private ValueConverter valueConverter; - private EvaluationContextConverter evaluationContextConverter; + private final LDLogger logger; + private final EvaluationDetailConverter evaluationDetailConverter; + private final ValueConverter valueConverter; + private final EvaluationContextConverter evaluationContextConverter; - private LDClientInterface client; + private final LDClientInterface client; /** * Create a provider with the given LaunchDarkly client and provider configuration. diff --git a/src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAContextConverter.java b/src/test/java/com/launchdarkly/openfeature/serverprovider/ContextConverterTest.java similarity index 93% rename from src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAContextConverter.java rename to src/test/java/com/launchdarkly/openfeature/serverprovider/ContextConverterTest.java index 4690225..fe4d9f2 100644 --- a/src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAContextConverter.java +++ b/src/test/java/com/launchdarkly/openfeature/serverprovider/ContextConverterTest.java @@ -15,7 +15,7 @@ import static org.junit.Assert.*; -public class GivenAContextConverter { +public class ContextConverterTest { TestLogger testLogger = new TestLogger(); EvaluationContextConverter evaluationContextConverter = new EvaluationContextConverter( LDLogger.withAdapter(testLogger, "test-logger") @@ -31,7 +31,7 @@ public void itCanCreateAContextFromAKeyOnly() { LDContext converted = evaluationContextConverter.toLdContext(new ImmutableContext("the-key")); assertEquals(expectedContext, converted); - HashMap attributes = new HashMap(); + HashMap attributes = new HashMap<>(); attributes.put("key", new Value("the-key")); LDContext convertedKey = evaluationContextConverter.toLdContext(new ImmutableContext(attributes)); assertEquals(expectedContext, convertedKey); @@ -44,7 +44,7 @@ public void itCanCreateAContextFromAKeyAndKind() { LDContext expectedContext = LDContext.builder(ContextKind.of("organization"), "org-key").build(); LDContext converted = evaluationContextConverter - .toLdContext(new ImmutableContext("org-key", new HashMap() {{ + .toLdContext(new ImmutableContext("org-key", new HashMap() {{ put("kind", new Value("organization")); }})); @@ -66,7 +66,7 @@ public void itLogsAnErrorWhenThereIsNoTargetingKey() { public void itGivesTargetingKeyPrecedence() { LDContext expectedContext = LDContext.builder("key-to-use").build(); - HashMap attributes = new HashMap(); + HashMap attributes = new HashMap<>(); attributes.put("key", new Value("key-not-to-use")); LDContext converted = evaluationContextConverter.toLdContext( @@ -80,7 +80,7 @@ public void itGivesTargetingKeyPrecedence() { @Test public void itHandlesAKeyOfIncorrectType() { - HashMap attributes = new HashMap(); + HashMap attributes = new HashMap<>(); attributes.put("key", new Value(0)); evaluationContextConverter.toLdContext( @@ -98,7 +98,7 @@ public void itHandlesAKeyOfIncorrectType() { public void itHandlesInvalidBuiltInAttributes() { LDContext expectedContext = LDContext.builder("user-key").build(); - HashMap attributes = new HashMap(); + HashMap attributes = new HashMap<>(); attributes.put("name", new Value(3)); attributes.put("anonymous", new Value("potato")); // The attributes were not valid, so they should be discarded. @@ -123,7 +123,7 @@ public void itHandlesValidBuiltInAttributes() { .anonymous(true) .build(); - HashMap attributes = new HashMap(); + HashMap attributes = new HashMap<>(); attributes.put("name", new Value("the-name")); attributes.put("anonymous", new Value(true)); @@ -148,9 +148,9 @@ public void itCanCreateAValidMultiKindContext() { .build() ); - EvaluationContext evaluationContext = new ImmutableContext(new HashMap() {{ + EvaluationContext evaluationContext = new ImmutableContext(new HashMap() {{ put("kind", new Value("multi")); - put("organization", new Value(new ImmutableStructure(new HashMap() {{ + put("organization", new Value(new ImmutableStructure(new HashMap() {{ put("name", new Value("the-org-name")); put("targetingKey", new Value("my-org-key")); put("myCustomAttribute", new Value("myAttributeValue")); @@ -158,7 +158,7 @@ public void itCanCreateAValidMultiKindContext() { add(new Value("myCustomAttribute")); }})); }}))); - put("user", new Value(new ImmutableStructure(new HashMap() {{ + put("user", new Value(new ImmutableStructure(new HashMap() {{ put("key", new Value("my-user-key")); put("anonymous", new Value(true)); put("myCustomAttribute", new Value("myAttributeValue")); diff --git a/src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAnEvaluationDetailConverter.java b/src/test/java/com/launchdarkly/openfeature/serverprovider/EvaluationDetailConverterTest.java similarity index 96% rename from src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAnEvaluationDetailConverter.java rename to src/test/java/com/launchdarkly/openfeature/serverprovider/EvaluationDetailConverterTest.java index 121ffa1..21c147a 100644 --- a/src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAnEvaluationDetailConverter.java +++ b/src/test/java/com/launchdarkly/openfeature/serverprovider/EvaluationDetailConverterTest.java @@ -9,17 +9,13 @@ import static org.junit.Assert.*; -public class GivenAnEvaluationDetailConverter { +public class EvaluationDetailConverterTest { private final Double EPSILON = 0.00001; TestLogger testLogger = new TestLogger(); EvaluationDetailConverter evaluationDetailConverter = new EvaluationDetailConverter( LDLogger.withAdapter(testLogger, "test-logger") ); - private TestLogger.TestChannel logs() { - return testLogger.getChannel("test-logger"); - } - @Test public void itCanConvertDoubleEvaluationDetail() { EvaluationDetail inputDetail = EvaluationDetail.fromValue( @@ -27,7 +23,7 @@ public void itCanConvertDoubleEvaluationDetail() { ProviderEvaluation converted = evaluationDetailConverter.toEvaluationDetails(inputDetail); - assertEquals(3.0, converted.getValue().doubleValue(), EPSILON); + assertEquals(3.0, converted.getValue(), EPSILON); assertEquals("17", converted.getVariant()); assertEquals(Reason.DISABLED.toString(), converted.getReason()); } diff --git a/src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAnLdValueConverter.java b/src/test/java/com/launchdarkly/openfeature/serverprovider/LdValueConverterTest.java similarity index 84% rename from src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAnLdValueConverter.java rename to src/test/java/com/launchdarkly/openfeature/serverprovider/LdValueConverterTest.java index 2222d4f..e72950e 100644 --- a/src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAnLdValueConverter.java +++ b/src/test/java/com/launchdarkly/openfeature/serverprovider/LdValueConverterTest.java @@ -4,19 +4,16 @@ import com.launchdarkly.sdk.ArrayBuilder; import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.ObjectBuilder; -import dev.openfeature.sdk.ImmutableStructure; import dev.openfeature.sdk.Structure; import dev.openfeature.sdk.Value; import org.junit.Test; -import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import static org.junit.Assert.*; -public class GivenAnLdValueConverter { - private LDValueConverter valueConverter = new LDValueConverter(LDLogger.none()); +public class LdValueConverterTest { + private final LDValueConverter valueConverter = new LDValueConverter(LDLogger.none()); private final Double EPSILON = 0.00001; @Test @@ -77,13 +74,6 @@ public void itCanConvertLists() { @Test public void itCanConvertStructures() { - Value ofValueStructure = new Value(new ImmutableStructure(new HashMap(){{ - put("aKey", new Value("aValue")); - put("structKey", new Value(new ImmutableStructure(new HashMap(){{ - put("bKey", new Value("bValue")); - }}))); - }})); - LDValue ldStructValue = new ObjectBuilder() .put("aKey", "aValue") .put("structKey", new ObjectBuilder().put("bKey", "bValue").build()).build(); diff --git a/src/test/java/com/launchdarkly/openfeature/serverprovider/ProviderConfigurationTests.java b/src/test/java/com/launchdarkly/openfeature/serverprovider/ProviderConfigurationTests.java index 46f71b0..e23ac67 100644 --- a/src/test/java/com/launchdarkly/openfeature/serverprovider/ProviderConfigurationTests.java +++ b/src/test/java/com/launchdarkly/openfeature/serverprovider/ProviderConfigurationTests.java @@ -1,13 +1,13 @@ package com.launchdarkly.openfeature.serverprovider; -import com.launchdarkly.logging.LDLogAdapter; import com.launchdarkly.logging.LDLogLevel; import com.launchdarkly.logging.LDLogger; import com.launchdarkly.sdk.server.Components; import com.launchdarkly.sdk.server.subsystems.LoggingConfiguration; import org.junit.Test; -import static org.junit.Assert.*; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; public class ProviderConfigurationTests { diff --git a/src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAProviderWithMockLdClient.java b/src/test/java/com/launchdarkly/openfeature/serverprovider/ProviderTest.java similarity index 99% rename from src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAProviderWithMockLdClient.java rename to src/test/java/com/launchdarkly/openfeature/serverprovider/ProviderTest.java index c4dc6f6..7194764 100644 --- a/src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAProviderWithMockLdClient.java +++ b/src/test/java/com/launchdarkly/openfeature/serverprovider/ProviderTest.java @@ -11,7 +11,7 @@ import static org.mockito.Mockito.*; import static org.junit.Assert.*; -public class GivenAProviderWithMockLdClient { +public class ProviderTest { LDClientInterface mockedLdClient = mock(LDClientInterface.class); Provider ldProvider = new Provider(mockedLdClient); diff --git a/src/test/java/com/launchdarkly/openfeature/serverprovider/TestLogger.java b/src/test/java/com/launchdarkly/openfeature/serverprovider/TestLogger.java index fddf8eb..be2e771 100644 --- a/src/test/java/com/launchdarkly/openfeature/serverprovider/TestLogger.java +++ b/src/test/java/com/launchdarkly/openfeature/serverprovider/TestLogger.java @@ -2,7 +2,6 @@ import com.launchdarkly.logging.LDLogAdapter; import com.launchdarkly.logging.LDLogLevel; -import jdk.nashorn.internal.runtime.regexp.joni.Regex; import java.util.ArrayList; import java.util.HashMap; @@ -12,16 +11,15 @@ * and validate the content of those messages. */ class TestLogger implements LDLogAdapter { - private HashMap channels = new HashMap<>(); + private final HashMap channels = new HashMap<>(); public TestChannel getChannel(String name) { return channels.get(name); } - public class TestChannel implements Channel { - private String name; + public static class TestChannel implements Channel { - private HashMap> messages = new HashMap(); + private final HashMap> messages = new HashMap<>(); public int countForLevel(LDLogLevel level) { if (messages.containsKey(level)) { @@ -32,9 +30,7 @@ public int countForLevel(LDLogLevel level) { public boolean expectedMessageInLevel(LDLogLevel level, String regexString) { if (messages.containsKey(level)) { - return messages.get(level).stream().anyMatch(value -> { - return value.matches(regexString); - }); + return messages.get(level).stream().anyMatch(value -> value.matches(regexString)); } return false; } @@ -43,12 +39,11 @@ public boolean containsAnyLogs() { return messages.size() != 0; } - private TestChannel(String name) { - this.name = name; + private TestChannel(String ignoredName) { } private void addMessage(LDLogLevel ldLogLevel, String message) { - ArrayList forLevel = messages.getOrDefault(ldLogLevel, new ArrayList()); + ArrayList forLevel = messages.getOrDefault(ldLogLevel, new ArrayList<>()); forLevel.add(message); diff --git a/src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAValueConverter.java b/src/test/java/com/launchdarkly/openfeature/serverprovider/ValueConverterTest.java similarity index 92% rename from src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAValueConverter.java rename to src/test/java/com/launchdarkly/openfeature/serverprovider/ValueConverterTest.java index 2a7ecd9..a60189a 100644 --- a/src/test/java/com/launchdarkly/openfeature/serverprovider/GivenAValueConverter.java +++ b/src/test/java/com/launchdarkly/openfeature/serverprovider/ValueConverterTest.java @@ -14,8 +14,8 @@ import static org.junit.Assert.*; -public class GivenAValueConverter { - private ValueConverter valueConverter = new ValueConverter(LDLogger.none()); +public class ValueConverterTest { + private final ValueConverter valueConverter = new ValueConverter(LDLogger.none()); private final Double EPSILON = 0.00001; @Test @@ -70,7 +70,7 @@ public void itCanConvertLists() { }}); LDValue ldValue = valueConverter.toLdValue(ofValueList); - List ldValueList = new ArrayList(); + List ldValueList = new ArrayList<>(); ldValue.values().forEach(ldValueList::add); assertEquals(5, ldValueList.size()); @@ -93,10 +93,10 @@ public void itCanConvertStructures() { LDValue ldValue = valueConverter.toLdValue(ofValueStructure); - List keyList = new ArrayList(); + List keyList = new ArrayList<>(); ldValue.keys().forEach(keyList::add); - List valueList = new ArrayList(); + List valueList = new ArrayList<>(); ldValue.values().forEach(valueList::add); assertEquals("aKey", keyList.get(0));