feat: dispatch NamedAction and NamedCondition by key
Hosts register one bean per action/condition; Spring wires the dispatcher when no facade is defined, and REST catalog projects from those beans. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
package com.jetlumen.ordo.core.spi;
|
||||
|
||||
import com.jetlumen.ordo.api.ActionHandler;
|
||||
import com.jetlumen.ordo.api.NamedAction;
|
||||
import com.jetlumen.ordo.api.ProcessRuntime;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** {@link ActionHandler} that dispatches by {@link NamedAction#key()}. */
|
||||
public final class DispatchingActionHandler implements ActionHandler {
|
||||
private final Map<String, NamedAction> actions;
|
||||
|
||||
private DispatchingActionHandler(Map<String, NamedAction> actions) {
|
||||
this.actions = actions;
|
||||
}
|
||||
|
||||
public static DispatchingActionHandler of(List<? extends NamedAction> actions) {
|
||||
return new DispatchingActionHandler(NamedHandlers.indexByKey(actions, NamedAction::key, "action"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(String actionKey, ProcessRuntime runtime) {
|
||||
NamedAction action = actions.get(NamedHandlers.lookupKey(actionKey));
|
||||
if (action == null) {
|
||||
throw new IllegalArgumentException("unknown action: " + actionKey);
|
||||
}
|
||||
action.execute(runtime);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.jetlumen.ordo.core.spi;
|
||||
|
||||
import com.jetlumen.ordo.api.NamedCondition;
|
||||
import com.jetlumen.ordo.api.ProcessRuntime;
|
||||
import com.jetlumen.ordo.api.RoutingCondition;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** {@link RoutingCondition} that dispatches by {@link NamedCondition#key()}. Unknown keys are false. */
|
||||
public final class DispatchingRoutingCondition implements RoutingCondition {
|
||||
private final Map<String, NamedCondition> conditions;
|
||||
|
||||
private DispatchingRoutingCondition(Map<String, NamedCondition> conditions) {
|
||||
this.conditions = conditions;
|
||||
}
|
||||
|
||||
public static DispatchingRoutingCondition of(List<? extends NamedCondition> conditions) {
|
||||
return new DispatchingRoutingCondition(NamedHandlers.indexByKey(conditions, NamedCondition::key, "condition"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String conditionKey, Map<String, Object> args, ProcessRuntime runtime) {
|
||||
NamedCondition condition = conditions.get(NamedHandlers.lookupKey(conditionKey));
|
||||
if (condition == null) {
|
||||
return false;
|
||||
}
|
||||
return condition.matches(args, runtime);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.jetlumen.ordo.core.spi;
|
||||
|
||||
import com.jetlumen.ordo.api.Texts;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
|
||||
final class NamedHandlers {
|
||||
private NamedHandlers() {
|
||||
}
|
||||
|
||||
static String requireKey(String key) {
|
||||
Texts.requireText(key, "key");
|
||||
return key.strip();
|
||||
}
|
||||
|
||||
static String lookupKey(String key) {
|
||||
return key == null ? "" : key.strip();
|
||||
}
|
||||
|
||||
static <T> Map<String, T> indexByKey(List<? extends T> items, Function<T, String> keyExtractor, String kind) {
|
||||
Objects.requireNonNull(items, kind + "s must not be null");
|
||||
Map<String, T> map = new LinkedHashMap<>();
|
||||
for (T item : items) {
|
||||
Objects.requireNonNull(item, kind + " must not be null");
|
||||
String key = requireKey(keyExtractor.apply(item));
|
||||
if (map.put(key, item) != null) {
|
||||
throw new IllegalStateException("duplicate " + kind + " key: " + key);
|
||||
}
|
||||
}
|
||||
return Map.copyOf(map);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.jetlumen.ordo.core.spi;
|
||||
|
||||
import com.jetlumen.ordo.api.NamedAction;
|
||||
import com.jetlumen.ordo.api.NamedCondition;
|
||||
import com.jetlumen.ordo.api.OrdoCatalog;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/** Catalog projected from registered {@link NamedCondition} and {@link NamedAction} instances. */
|
||||
public final class RegistryOrdoCatalog implements OrdoCatalog {
|
||||
private final List<Item> conditions;
|
||||
private final List<Item> actions;
|
||||
|
||||
public RegistryOrdoCatalog(List<? extends NamedCondition> conditions, List<? extends NamedAction> actions) {
|
||||
this.conditions = Objects.requireNonNull(conditions, "conditions must not be null").stream()
|
||||
.map(NamedCondition::catalogItem)
|
||||
.toList();
|
||||
this.actions = Objects.requireNonNull(actions, "actions must not be null").stream()
|
||||
.map(NamedAction::catalogItem)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Item> conditions() {
|
||||
return conditions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Item> actions() {
|
||||
return actions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Item> assignees() {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package com.jetlumen.ordo.core.spi;
|
||||
|
||||
import com.jetlumen.ordo.api.NamedAction;
|
||||
import com.jetlumen.ordo.api.NamedCondition;
|
||||
import com.jetlumen.ordo.api.ProcessContext;
|
||||
import com.jetlumen.ordo.api.ProcessRuntime;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class DispatchingHandlersTest {
|
||||
|
||||
private static final ProcessRuntime RUNTIME =
|
||||
new ProcessRuntime("inst", "def", 1, "alice", "step", ProcessContext.empty());
|
||||
|
||||
@Test
|
||||
void actionDispatchesByKey() {
|
||||
List<String> executed = new ArrayList<>();
|
||||
DispatchingActionHandler.of(List.of(action("mail", executed::add)))
|
||||
.execute("mail", RUNTIME);
|
||||
assertEquals(List.of("mail"), executed);
|
||||
}
|
||||
|
||||
@Test
|
||||
void actionStripsKeyOnRegisterAndLookup() {
|
||||
List<String> executed = new ArrayList<>();
|
||||
DispatchingActionHandler.of(List.of(action(" mail ", executed::add)))
|
||||
.execute("mail", RUNTIME);
|
||||
assertEquals(List.of(" mail "), executed);
|
||||
}
|
||||
|
||||
@Test
|
||||
void unknownActionThrows() {
|
||||
DispatchingActionHandler handler = DispatchingActionHandler.of(List.of(action("mail", key -> {
|
||||
})));
|
||||
IllegalArgumentException error = assertThrows(IllegalArgumentException.class,
|
||||
() -> handler.execute("other", RUNTIME));
|
||||
assertEquals("unknown action: other", error.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void duplicateActionKeyFailsFast() {
|
||||
IllegalStateException error = assertThrows(IllegalStateException.class,
|
||||
() -> DispatchingActionHandler.of(List.of(action("mail", key -> {
|
||||
}), action("mail", key -> {
|
||||
}))));
|
||||
assertEquals("duplicate action key: mail", error.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void blankActionKeyRejected() {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> DispatchingActionHandler.of(List.of(action(" ", key -> {
|
||||
}))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void conditionDispatchesByKey() {
|
||||
DispatchingRoutingCondition condition = DispatchingRoutingCondition.of(List.of(
|
||||
namedCondition("amountGt", (args, runtime) -> ((Number) args.get("threshold")).intValue() < 10)));
|
||||
assertTrue(condition.matches("amountGt", Map.of("threshold", 5), RUNTIME));
|
||||
assertFalse(condition.matches("amountGt", Map.of("threshold", 20), RUNTIME));
|
||||
}
|
||||
|
||||
@Test
|
||||
void unknownConditionIsFalse() {
|
||||
DispatchingRoutingCondition condition = DispatchingRoutingCondition.of(List.of(
|
||||
namedCondition("amountGt", (args, runtime) -> true)));
|
||||
assertFalse(condition.matches("missing", Map.of(), RUNTIME));
|
||||
}
|
||||
|
||||
@Test
|
||||
void duplicateConditionKeyFailsFast() {
|
||||
IllegalStateException error = assertThrows(IllegalStateException.class,
|
||||
() -> DispatchingRoutingCondition.of(List.of(
|
||||
namedCondition("amountGt", (args, runtime) -> true),
|
||||
namedCondition("amountGt", (args, runtime) -> false))));
|
||||
assertEquals("duplicate condition key: amountGt", error.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void blankConditionKeyRejected() {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> DispatchingRoutingCondition.of(List.of(namedCondition("", (args, runtime) -> true))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void catalogProjectsNamedHandlers() {
|
||||
RegistryOrdoCatalog catalog = new RegistryOrdoCatalog(
|
||||
List.of(namedCondition("amountGt", (args, runtime) -> true)),
|
||||
List.of(action("mail", key -> {
|
||||
})));
|
||||
assertEquals("amountGt", catalog.conditions().get(0).key());
|
||||
assertEquals("mail", catalog.actions().get(0).key());
|
||||
assertTrue(catalog.assignees().isEmpty());
|
||||
}
|
||||
|
||||
private static NamedAction action(String key, Consumer<String> onExecute) {
|
||||
return new NamedAction() {
|
||||
@Override
|
||||
public String key() {
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(ProcessRuntime runtime) {
|
||||
onExecute.accept(key);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static NamedCondition namedCondition(String key, BiFunction<Map<String, Object>, ProcessRuntime, Boolean> delegate) {
|
||||
return new NamedCondition() {
|
||||
@Override
|
||||
public String key() {
|
||||
return key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(Map<String, Object> args, ProcessRuntime runtime) {
|
||||
return delegate.apply(args, runtime);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user