-
Notifications
You must be signed in to change notification settings - Fork 46
feat: Added hook data #1506
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: Added hook data #1506
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
package dev.openfeature.sdk; | ||
|
||
import java.util.Collections; | ||
import java.util.HashMap; | ||
import java.util.Map; | ||
|
||
/** | ||
* Hook data provides a way for hooks to maintain state across their execution stages. | ||
* Each hook instance gets its own isolated data store that persists only for the duration | ||
* of a single flag evaluation. | ||
*/ | ||
public interface HookData { | ||
|
||
/** | ||
* Sets a value for the given key. | ||
* | ||
* @param key the key to store the value under | ||
* @param value the value to store | ||
*/ | ||
void set(String key, Object value); | ||
|
||
/** | ||
* Gets the value for the given key. | ||
* | ||
* @param key the key to retrieve the value for | ||
* @return the value, or null if not found | ||
*/ | ||
Object get(String key); | ||
|
||
/** | ||
* Gets the value for the given key, cast to the specified type. | ||
* | ||
* @param <T> the type to cast to | ||
* @param key the key to retrieve the value for | ||
* @param type the class to cast to | ||
* @return the value cast to the specified type, or null if not found | ||
* @throws ClassCastException if the value cannot be cast to the specified type | ||
*/ | ||
<T> T get(String key, Class<T> type); | ||
|
||
/** | ||
* Default implementation using ConcurrentHashMap for thread safety. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
*/ | ||
static HookData create() { | ||
return new DefaultHookData(); | ||
} | ||
|
||
/** | ||
* Default thread-safe implementation of HookData. | ||
*/ | ||
public class DefaultHookData implements HookData { | ||
private final Map<String, Object> data = Collections.synchronizedMap(new HashMap<>()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need a thread safe implementation? If the hook data only lives for the duraion of a flag evalution, there will probably not be concurrent access to this object. IIRC, a flag evaluation happens synchronously. |
||
|
||
@Override | ||
public void set(String key, Object value) { | ||
data.put(key, value); | ||
} | ||
|
||
@Override | ||
public Object get(String key) { | ||
return data.get(key); | ||
} | ||
|
||
@Override | ||
public <T> T get(String key, Class<T> type) { | ||
Object value = data.get(key); | ||
if (value == null) { | ||
return null; | ||
} | ||
if (!type.isInstance(value)) { | ||
throw new ClassCastException("Value for key '" + key + "' is not of type " + type.getName()); | ||
} | ||
return type.cast(value); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
package dev.openfeature.sdk; | ||
|
||
import static org.junit.jupiter.api.Assertions.*; | ||
|
||
import java.util.concurrent.CountDownLatch; | ||
import java.util.concurrent.ExecutorService; | ||
import java.util.concurrent.Executors; | ||
import java.util.concurrent.TimeUnit; | ||
import org.junit.jupiter.api.Test; | ||
|
||
class HookDataTest { | ||
|
||
@Test | ||
void shouldStoreAndRetrieveValues() { | ||
HookData hookData = HookData.create(); | ||
|
||
hookData.set("key1", "value1"); | ||
hookData.set("key2", 42); | ||
hookData.set("key3", true); | ||
|
||
assertEquals("value1", hookData.get("key1")); | ||
assertEquals(42, hookData.get("key2")); | ||
assertEquals(true, hookData.get("key3")); | ||
} | ||
|
||
@Test | ||
void shouldReturnNullForMissingKeys() { | ||
HookData hookData = HookData.create(); | ||
|
||
assertNull(hookData.get("nonexistent")); | ||
} | ||
|
||
@Test | ||
void shouldSupportTypeSafeRetrieval() { | ||
HookData hookData = HookData.create(); | ||
|
||
hookData.set("string", "hello"); | ||
hookData.set("integer", 123); | ||
hookData.set("boolean", false); | ||
|
||
assertEquals("hello", hookData.get("string", String.class)); | ||
assertEquals(Integer.valueOf(123), hookData.get("integer", Integer.class)); | ||
assertEquals(Boolean.FALSE, hookData.get("boolean", Boolean.class)); | ||
} | ||
|
||
@Test | ||
void shouldReturnNullForMissingKeysWithType() { | ||
HookData hookData = HookData.create(); | ||
|
||
assertNull(hookData.get("missing", String.class)); | ||
} | ||
|
||
@Test | ||
void shouldThrowClassCastExceptionForWrongType() { | ||
HookData hookData = HookData.create(); | ||
|
||
hookData.set("string", "not a number"); | ||
|
||
assertThrows(ClassCastException.class, () -> { | ||
hookData.get("string", Integer.class); | ||
}); | ||
} | ||
|
||
@Test | ||
void shouldOverwriteExistingValues() { | ||
HookData hookData = HookData.create(); | ||
|
||
hookData.set("key", "original"); | ||
assertEquals("original", hookData.get("key")); | ||
|
||
hookData.set("key", "updated"); | ||
assertEquals("updated", hookData.get("key")); | ||
} | ||
|
||
@Test | ||
void shouldBeThreadSafe() throws InterruptedException { | ||
HookData hookData = HookData.create(); | ||
int threadCount = 10; | ||
int operationsPerThread = 100; | ||
CountDownLatch latch = new CountDownLatch(threadCount); | ||
ExecutorService executor = Executors.newFixedThreadPool(threadCount); | ||
|
||
for (int i = 0; i < threadCount; i++) { | ||
final int threadId = i; | ||
executor.submit(() -> { | ||
try { | ||
for (int j = 0; j < operationsPerThread; j++) { | ||
String key = "thread-" + threadId + "-key-" + j; | ||
String value = "thread-" + threadId + "-value-" + j; | ||
hookData.set(key, value); | ||
assertEquals(value, hookData.get(key)); | ||
} | ||
} finally { | ||
latch.countDown(); | ||
} | ||
}); | ||
} | ||
|
||
assertTrue(latch.await(10, TimeUnit.SECONDS)); | ||
executor.shutdown(); | ||
} | ||
|
||
@Test | ||
void shouldSupportNullValues() { | ||
HookData hookData = HookData.create(); | ||
|
||
hookData.set("nullKey", null); | ||
assertNull(hookData.get("nullKey")); | ||
assertNull(hookData.get("nullKey", String.class)); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With this addition, the Constructor of the
HookContext
changes, which may be a breaking change @toddbaert