Skip to content

Commit

Permalink
Bosk.registerHooks
Browse files Browse the repository at this point in the history
  • Loading branch information
prdoyle committed Oct 13, 2023
1 parent 03d8ffa commit ca90407
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 0 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);
}
});
}

}
}
28 changes: 28 additions & 0 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 @@ -473,6 +474,33 @@ void nested_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

0 comments on commit ca90407

Please sign in to comment.