Skip to content

Commit

Permalink
Hooks by annotation (#77)
Browse files Browse the repository at this point in the history
  • Loading branch information
prdoyle authored Oct 13, 2023
2 parents 786e7d2 + ca90407 commit 5566f0b
Show file tree
Hide file tree
Showing 9 changed files with 121 additions and 28 deletions.
4 changes: 4 additions & 0 deletions bosk-core/src/main/java/io/vena/bosk/Bosk.java
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,10 @@ public <T> void registerHook(String name, @NonNull Reference<T> scope, @NonNull
localDriver.triggerEverywhere(reg);
}

public void registerHooks(Object receiver) throws InvalidTypeException {
HookRegistrar.registerHooks(receiver, this);
}

public List<HookRegistration<?>> allRegisteredHooks() {
return unmodifiableList(hooks);
}
Expand Down
59 changes: 59 additions & 0 deletions bosk-core/src/main/java/io/vena/bosk/HookRegistrar.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package io.vena.bosk;

import io.vena.bosk.annotations.ReferencePath;
import io.vena.bosk.exceptions.InvalidTypeException;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
class HookRegistrar {
@SuppressWarnings({"unchecked","rawtypes"})
static <T> void registerHooks(T receiverObject, Bosk<?> bosk) throws InvalidTypeException {
Class<?> receiverClass = receiverObject.getClass();
for (Method method: receiverClass.getDeclaredMethods()) { // TODO: Inherited methods
ReferencePath referencePath = method.getAnnotation(ReferencePath.class);
if (referencePath == null) {
continue;
}
Path path = Path.parseParameterized(referencePath.value());
Reference<Object> scope = bosk.rootReference().then(Object.class, path);
List<Function<Reference<?>, Object>> argumentFunctions = new ArrayList<>(method.getParameterCount());
argumentFunctions.add(ref -> receiverObject); // The "this" pointer
for (Parameter p: method.getParameters()) {
if (p.getType().isAssignableFrom(Reference.class)) {
if (ReferenceUtils.parameterType(p.getParameterizedType(), Reference.class, 0).equals(scope.targetType())) {
argumentFunctions.add(ref -> ref);
} else {
throw new InvalidTypeException("Expected reference to " + scope.targetType() + ": " + method.getName() + " parameter " + p.getName());
}
} else if (p.getType().isAssignableFrom(BindingEnvironment.class)) {
argumentFunctions.add(ref -> scope.parametersFrom(ref.path()));
} else {
throw new InvalidTypeException("Unsupported parameter type " + p.getType() + ": " + method.getName() + " parameter " + p.getName());
}
}
MethodHandle hook;
try {
hook = MethodHandles.lookup().unreflect(method);
} catch (IllegalAccessException e) {
throw new IllegalArgumentException(e);
}
bosk.registerHook(method.getName(), scope, ref -> {
try {
List<Object> arguments = new ArrayList<>(argumentFunctions.size());
argumentFunctions.forEach(f -> arguments.add(f.apply(ref)));
hook.invokeWithArguments(arguments);
} catch (Throwable e) {
throw new IllegalStateException("Unable to call hook \"" + method.getName() + "\"", e);
}
});
}

}
}
4 changes: 2 additions & 2 deletions bosk-core/src/main/java/io/vena/bosk/ReferenceBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

class ReferenceBuilder {
@SuppressWarnings({"unchecked","rawtypes"})
static <T, R extends StateTreeNode> T buildReferences(Class<T> refsClass, Bosk<R> bosk) throws InvalidTypeException {
static <T> T buildReferences(Class<T> refsClass, Bosk<?> bosk) throws InvalidTypeException {
ClassBuilder<T> cb = new ClassBuilder<>(
"REFS_" + refsClass.getSimpleName(),
refsClass,
Expand All @@ -24,7 +24,7 @@ static <T, R extends StateTreeNode> T buildReferences(Class<T> refsClass, Bosk<R

cb.beginClass();

for (Method method: refsClass.getDeclaredMethods()) {
for (Method method: refsClass.getDeclaredMethods()) { // TODO: Inherited methods
ReferencePath referencePath = method.getAnnotation(ReferencePath.class);
if (referencePath == null) {
throw new InvalidTypeException("Missing " + ReferencePath.class.getSimpleName() + " annotation on " + methodName(method));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
Expand Down Expand Up @@ -333,9 +334,9 @@ private void checkEntityReference(Reference<TestEntity> ref, Path expectedPath,

try (val __ = bosk.readContext()) {
if (expectedValue == null) {
assertEquals(null, ref.then(Catalog.class, TestEntity.Fields.catalog).valueIfExists());
assertEquals(null, ref.then(Listing.class, TestEntity.Fields.listing).valueIfExists());
assertEquals(null, ref.then(SideTable.class, TestEntity.Fields.sideTable).valueIfExists());
assertNull(ref.then(Catalog.class, TestEntity.Fields.catalog).valueIfExists());
assertNull(ref.then(Listing.class, TestEntity.Fields.listing).valueIfExists());
assertNull(ref.then(SideTable.class, TestEntity.Fields.sideTable).valueIfExists());
} else {
assertEquals(expectedValue.catalog(), ref.then(Catalog.class, TestEntity.Fields.catalog).value());
assertEquals(expectedValue.listing(), ref.then(Listing.class, TestEntity.Fields.listing).value());
Expand Down
64 changes: 46 additions & 18 deletions bosk-core/src/test/java/io/vena/bosk/HooksTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import io.vena.bosk.annotations.ReferencePath;
import io.vena.bosk.exceptions.InvalidTypeException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import lombok.val;
Expand Down Expand Up @@ -56,7 +57,7 @@ void setupBosk() throws InvalidTypeException {

@ParameterizedTest
@EnumSource(Variant.class)
void testNothingBeforeRegistration(Variant variant) {
void beforeRegistration_noHooks(Variant variant) {
variant.submit.replacement(bosk, refs, refs.childString(child2), "Too early");
assertEquals(emptyList(), recorder.events(), "Hook shouldn't see updates unless they are registered");
}
Expand All @@ -67,7 +68,7 @@ void testNothingBeforeRegistration(Variant variant) {
//

@Test
void testBasic_hooksRunWhenRegistered() {
void basic_hooksRunWhenRegistered() {
bosk.registerHook("child2", refs.child(child2), recorder.hookNamed("child2"));
assertEquals(
singletonList(
Expand All @@ -78,7 +79,7 @@ void testBasic_hooksRunWhenRegistered() {

@ParameterizedTest
@EnumSource(Variant.class)
void testBasic_noIrrelevantHooks(Variant variant) {
void basic_noIrrelevantHooks(Variant variant) {
bosk.registerHook("child2", refs.child(child2), recorder.hookNamed("child2"));
recorder.restart();
variant.submit.replacement(bosk, refs, refs.childString(child1), "Child 1 only");
Expand All @@ -87,7 +88,7 @@ void testBasic_noIrrelevantHooks(Variant variant) {

@ParameterizedTest
@EnumSource(Variant.class)
void testBasic_objectReplacement(Variant variant) {
void basic_objectReplacement(Variant variant) {
registerInterleavedHooks();

TestChild newChild = originalChild2.withString(originalChild2.string() + " v2");
Expand All @@ -99,7 +100,7 @@ void testBasic_objectReplacement(Variant variant) {

@ParameterizedTest
@EnumSource(Variant.class)
void testBasic_fieldReplacement(Variant variant) {
void basic_fieldReplacement(Variant variant) {
registerInterleavedHooks();

String stringV3 = originalChild2.string() + " v3";
Expand All @@ -112,7 +113,7 @@ void testBasic_fieldReplacement(Variant variant) {

@ParameterizedTest
@EnumSource(Variant.class)
void testBasic_parentReplacementSameChildren(Variant variant) {
void basic_parentReplacementSameChildren(Variant variant) {
registerInterleavedHooks();

TestEntity newParent = originalParent.withString(originalParent.string() + " v2");
Expand All @@ -128,7 +129,7 @@ void testBasic_parentReplacementSameChildren(Variant variant) {

@ParameterizedTest
@EnumSource(Variant.class)
void testBasic_parentReplacementReplacedChild(Variant variant) {
void basic_parentReplacementReplacedChild(Variant variant) {
registerInterleavedHooks();

String replacement = "replacement";
Expand All @@ -149,7 +150,7 @@ void testBasic_parentReplacementReplacedChild(Variant variant) {

@ParameterizedTest
@EnumSource(Variant.class)
void testBasic_parentReplacementChildDisappearedAndModified(Variant variant) {
void basic_parentReplacementChildDisappearedAndModified(Variant variant) {
registerInterleavedHooks();

TestChild newChild1 = originalChild1.withString("replacement1");
Expand Down Expand Up @@ -177,7 +178,7 @@ void testBasic_parentReplacementChildDisappearedAndModified(Variant variant) {

@ParameterizedTest
@EnumSource(Variant.class)
void testBasic_parentCreation(Variant variant) {
void basic_parentCreation(Variant variant) {
variant.submit.deletion(bosk, refs, refs.parent());
registerInterleavedHooks();
variant.submit.replacement(bosk, refs, refs.parent(), originalParent); // Time travel!
Expand All @@ -198,7 +199,7 @@ void testBasic_parentCreation(Variant variant) {

@ParameterizedTest
@EnumSource(Variant.class)
void testBasic_parentDeletion(Variant variant) {
void basic_parentDeletion(Variant variant) {
registerInterleavedHooks();
variant.submit.deletion(bosk, refs, refs.parent());

Expand All @@ -219,7 +220,7 @@ void testBasic_parentDeletion(Variant variant) {

@ParameterizedTest
@EnumSource(Variant.class)
void testBasic_objectDeletion(Variant variant) {
void basic_objectDeletion(Variant variant) {
registerInterleavedHooks();

variant.submit.deletion(bosk, refs, refs.child(child2));
Expand All @@ -237,7 +238,7 @@ void testBasic_objectDeletion(Variant variant) {

@ParameterizedTest
@EnumSource(Variant.class)
void testBasic_nonexistentObjectDeletion(Variant variant) {
void basic_nonexistentObjectDeletion(Variant variant) {
registerInterleavedHooks();

variant.submit.deletion(bosk, refs, refs.anyChild().boundTo(Identifier.from("nonexistent"), Identifier.from("child1")));
Expand All @@ -248,7 +249,7 @@ void testBasic_nonexistentObjectDeletion(Variant variant) {
}

@Test
void testBasic_initialization() {
void basic_initialization() {
registerInterleavedHooks();

Identifier child4ID = Identifier.from("child4");
Expand All @@ -268,7 +269,7 @@ void testBasic_initialization() {
}

@Test
void testBasic_reinitialization() {
void basic_reinitialization() {
registerInterleavedHooks();

TestChild newValue = originalChild1
Expand All @@ -283,7 +284,7 @@ void testBasic_reinitialization() {
}

@Test
void testBasic_initializationInNonexistentParent() {
void basic_initializationInNonexistentParent() {
registerInterleavedHooks();

Identifier child4ID = Identifier.from("child4");
Expand Down Expand Up @@ -329,7 +330,7 @@ private void checkInterleavedHooks(String message, TestEntity newParent, TestChi
//

@Test
void testNested_breadthFirst() throws IOException, InterruptedException {
void nested_breadthFirst() throws IOException, InterruptedException {
AtomicBoolean initializing = new AtomicBoolean(true);

// Child 1 update triggers A and B, and A triggers C
Expand Down Expand Up @@ -399,7 +400,7 @@ void testNested_breadthFirst() throws IOException, InterruptedException {
}

@Test
void testNestedMultipleUpdates_breadthFirst() {
void nestedMultipleUpdates_breadthFirst() {
// Register hooks to propagate string updates from parent -> child 1 -> 2 -> 3 with a tag
bosk.registerHook("+P", refs.parentString(), recorder.hookNamed("P", ref -> {
bosk.driver().submitReplacement(refs.childString(child1), ref.value() + "+P");
Expand Down Expand Up @@ -450,7 +451,7 @@ void testNestedMultipleUpdates_breadthFirst() {
}

@Test
void testNested_correctReadContext() {
void nested_correctReadContext() {
bosk.registerHook("stringCopier", refs.child(child2), recorder.hookNamed("stringCopier", ref ->
bosk.driver().submitReplacement(refs.childString(child1), ref.value().string())));
recorder.restart();
Expand All @@ -473,6 +474,33 @@ void testNested_correctReadContext() {
}
}

@Test
void registerHooks_works() throws InvalidTypeException {
HookReceiver receiver = new HookReceiver(bosk);
bosk.driver().submitReplacement(refs.childString(child1), "New value");
List<List<Object>> expected = asList(
// At registration time, the hook is called on all existing nodes
asList(refs.childString(child1), BindingEnvironment.singleton("child", child1), "child1"),
asList(refs.childString(child2), BindingEnvironment.singleton("child", child2), "child2"),
asList(refs.childString(child3), BindingEnvironment.singleton("child", child3), "child3"),
// Then the replacement causes another call
asList(refs.childString(child1), BindingEnvironment.singleton("child", child1), "New value")
);
assertEquals(expected, receiver.hookCalls);
}

public static class HookReceiver {
final List<List<Object>> hookCalls = new ArrayList<>();
public HookReceiver(Bosk<?> bosk) throws InvalidTypeException {
bosk.registerHooks(this);
}

@ReferencePath("/entities/parent/children/-child-/string")
void childStringChanged(Reference<String> ref, BindingEnvironment env) {
hookCalls.add(asList(ref, env, ref.valueIfExists()));
}
}

interface Submit {
<T> void replacement(Bosk<?> bosk, Refs refs, Reference<T> target, T newValue);
<T> void deletion(Bosk<?> bosk, Refs refs, Reference<T> target);
Expand Down
5 changes: 3 additions & 2 deletions bosk-core/src/test/java/io/vena/bosk/OptionalRefsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.StringContains.containsString;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;

class OptionalRefsTest extends AbstractRoundTripTest {
Expand Down Expand Up @@ -121,15 +122,15 @@ private <E extends Entity, V> void doTest(E initialRoot, ValueFactory<E, V> valu
@SuppressWarnings("unchecked")
Reference<V> optionalRef = bosk.rootReference().then((Class<V>)value.getClass(), "field");
try (val context = bosk.readContext()) {
assertEquals(null, optionalRef.valueIfExists());
assertNull(optionalRef.valueIfExists());
}
bosk.driver().submitReplacement(optionalRef, value);
try (val context = bosk.readContext()) {
assertEquals(value, optionalRef.valueIfExists());
}
bosk.driver().submitDeletion(optionalRef);
try (val context = bosk.readContext()) {
assertEquals(null, optionalRef.valueIfExists());
assertNull(optionalRef.valueIfExists());
}

// Try other ways of getting the same reference
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ void listValue_parameterizedElement_works() {
);
LinkedHashMap<Object, Object> expectedB = new LinkedHashMap<>();
expectedB.put("listOfA", asList(3.0, 4.0));
expectedB.put("listOfB", asList("string1, string2"));
expectedB.put("listOfB", singletonList("string1, string2"));
Map<String, Object> expected = new LinkedHashMap<>();
expected.put("listOfA", asList(1, 2));
expected.put("listOfB", singletonList(expectedB));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ private void connectionLoop() {
}
}
} catch (UnprocessableEventException|UnexpectedEventProcessingException e) {
LOGGER.warn("Unable to process MongoDB change event; reconnecting: {}", e.toString(), e);
LOGGER.warn("Unable to process MongoDB change event; reconnecting: {}", e, e);
listener.onDisconnect(e);
// Reconnection will skip this event, so it's safe to try it right away
continue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ static Stream<TestParameters.ParameterSet> parameters() {
));
}

enum FlushOrWait { FLUSH, WAIT };
enum FlushOrWait { FLUSH, WAIT }

@SuppressWarnings("unused")
static Stream<FlushOrWait> flushOrWait() {
Expand Down

0 comments on commit 5566f0b

Please sign in to comment.