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:
+11
-4
@@ -2,6 +2,8 @@ package com.jetlumen.ordo.spring.jdbc;
|
||||
|
||||
import com.jetlumen.ordo.api.ActionHandler;
|
||||
import com.jetlumen.ordo.api.AssigneeResolver;
|
||||
import com.jetlumen.ordo.api.NamedAction;
|
||||
import com.jetlumen.ordo.api.NamedCondition;
|
||||
import com.jetlumen.ordo.api.OrdoEngine;
|
||||
import com.jetlumen.ordo.api.OrdoEventListener;
|
||||
import com.jetlumen.ordo.api.RoutingCondition;
|
||||
@@ -13,6 +15,8 @@ import com.jetlumen.ordo.api.repository.ProcessDefinitionRepository;
|
||||
import com.jetlumen.ordo.api.repository.ProcessHistoryRepository;
|
||||
import com.jetlumen.ordo.api.repository.ProcessInstanceRepository;
|
||||
import com.jetlumen.ordo.core.DefaultOrdoEngine;
|
||||
import com.jetlumen.ordo.core.spi.DispatchingActionHandler;
|
||||
import com.jetlumen.ordo.core.spi.DispatchingRoutingCondition;
|
||||
import com.jetlumen.ordo.spring.OrdoProperties;
|
||||
import com.jetlumen.ordo.storage.jdbc.JdbcActionExecutionRepository;
|
||||
import com.jetlumen.ordo.storage.jdbc.JdbcApprovalTaskRepository;
|
||||
@@ -37,6 +41,7 @@ import org.springframework.context.annotation.Bean;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.time.Clock;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Autoconfigures a JDBC-backed {@link OrdoEngine} when a {@link DataSource} bean is
|
||||
@@ -72,14 +77,16 @@ public class OrdoJdbcAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public RoutingCondition ordoRoutingCondition() {
|
||||
return RoutingCondition.always();
|
||||
public RoutingCondition ordoRoutingCondition(ObjectProvider<NamedCondition> namedConditions) {
|
||||
List<NamedCondition> conditions = namedConditions.orderedStream().toList();
|
||||
return conditions.isEmpty() ? RoutingCondition.always() : DispatchingRoutingCondition.of(conditions);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public ActionHandler ordoActionHandler() {
|
||||
return ActionHandler.noop();
|
||||
public ActionHandler ordoActionHandler(ObjectProvider<NamedAction> namedActions) {
|
||||
List<NamedAction> actions = namedActions.orderedStream().toList();
|
||||
return actions.isEmpty() ? ActionHandler.noop() : DispatchingActionHandler.of(actions);
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
+14
-2
@@ -1,9 +1,13 @@
|
||||
package com.jetlumen.ordo.spring.rest;
|
||||
|
||||
import com.jetlumen.ordo.api.NamedAction;
|
||||
import com.jetlumen.ordo.api.NamedCondition;
|
||||
import com.jetlumen.ordo.api.OrdoCatalog;
|
||||
import com.jetlumen.ordo.api.OrdoEngine;
|
||||
import com.jetlumen.ordo.core.spi.RegistryOrdoCatalog;
|
||||
import com.jetlumen.ordo.spring.OrdoProperties;
|
||||
import com.jetlumen.ordo.spring.jdbc.OrdoJdbcAutoConfiguration;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
@@ -15,6 +19,8 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.web.servlet.DispatcherServlet;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@AutoConfiguration
|
||||
@ConditionalOnClass(DispatcherServlet.class)
|
||||
@ConditionalOnProperty(prefix = "ordo.rest", name = "enabled", havingValue = "true")
|
||||
@@ -33,7 +39,13 @@ public class OrdoRestAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public OrdoCatalog ordoCatalog() {
|
||||
return OrdoCatalog.empty();
|
||||
public OrdoCatalog ordoCatalog(ObjectProvider<NamedAction> namedActions,
|
||||
ObjectProvider<NamedCondition> namedConditions) {
|
||||
List<NamedAction> actions = namedActions.orderedStream().toList();
|
||||
List<NamedCondition> conditions = namedConditions.orderedStream().toList();
|
||||
if (actions.isEmpty() && conditions.isEmpty()) {
|
||||
return OrdoCatalog.empty();
|
||||
}
|
||||
return new RegistryOrdoCatalog(conditions, actions);
|
||||
}
|
||||
}
|
||||
|
||||
+95
@@ -4,15 +4,21 @@ import com.jetlumen.ordo.api.ActionHandler;
|
||||
import com.jetlumen.ordo.api.ApprovalStep;
|
||||
import com.jetlumen.ordo.api.ApprovalTask;
|
||||
import com.jetlumen.ordo.api.AssigneeResolver;
|
||||
import com.jetlumen.ordo.api.NamedAction;
|
||||
import com.jetlumen.ordo.api.NamedCondition;
|
||||
import com.jetlumen.ordo.api.OrdoEngine;
|
||||
import com.jetlumen.ordo.api.OrdoEventListener;
|
||||
import com.jetlumen.ordo.api.ProcessDefinition;
|
||||
import com.jetlumen.ordo.api.ProcessEvent;
|
||||
import com.jetlumen.ordo.api.ProcessEventType;
|
||||
import com.jetlumen.ordo.api.ProcessInstance;
|
||||
import com.jetlumen.ordo.api.ProcessRuntime;
|
||||
import com.jetlumen.ordo.api.RoutingCondition;
|
||||
import com.jetlumen.ordo.api.StepTransition;
|
||||
import com.jetlumen.ordo.api.query.PageRequest;
|
||||
import com.jetlumen.ordo.api.repository.ProcessDefinitionRepository;
|
||||
import com.jetlumen.ordo.core.spi.DispatchingActionHandler;
|
||||
import com.jetlumen.ordo.core.spi.DispatchingRoutingCondition;
|
||||
import com.jetlumen.ordo.storage.jdbc.dialect.SqlDialect;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
@@ -23,6 +29,7 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -168,6 +175,54 @@ class OrdoJdbcAutoConfigurationTest {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsNoopAndAlwaysWhenNoNamedHandlers() {
|
||||
withDataSourceRunner.run(context -> {
|
||||
context.getBean(ActionHandler.class).execute("any", null);
|
||||
assertThat(context.getBean(RoutingCondition.class).matches("any", Map.of(), null)).isTrue();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void assemblesDispatchingActionHandlerFromNamedActions() {
|
||||
withDataSourceRunner.withUserConfiguration(NamedActionConfig.class)
|
||||
.run(context -> {
|
||||
assertThat(context.getBean(ActionHandler.class)).isInstanceOf(DispatchingActionHandler.class);
|
||||
OrdoEngine engine = context.getBean(OrdoEngine.class);
|
||||
engine.publish(new ProcessDefinition("leave", "Leave",
|
||||
List.of(ApprovalStep.action("notify", "Notify", "leave-mail")),
|
||||
List.of(StepTransition.end("notify"))));
|
||||
engine.start("leave", "alice");
|
||||
assertThat(context.getBean(RecordingNamedAction.class).executed).containsExactly("leave-mail");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void honoursUserDefinedActionHandlerOverNamedActions() {
|
||||
withDataSourceRunner.withUserConfiguration(CustomActionHandlerConfig.class, NamedActionConfig.class)
|
||||
.run(context -> assertThatThrownBy(() -> context.getBean(ActionHandler.class).execute("any", null))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("custom-action"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void assemblesDispatchingRoutingConditionFromNamedConditions() {
|
||||
withDataSourceRunner.withUserConfiguration(NamedConditionConfig.class)
|
||||
.run(context -> {
|
||||
RoutingCondition routing = context.getBean(RoutingCondition.class);
|
||||
assertThat(routing).isInstanceOf(DispatchingRoutingCondition.class);
|
||||
assertThat(routing.matches("amountGt", Map.of(), null)).isTrue();
|
||||
assertThat(routing.matches("other", Map.of(), null)).isFalse();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void honoursUserDefinedRoutingConditionOverNamedConditions() {
|
||||
withDataSourceRunner.withUserConfiguration(CustomRoutingConditionConfig.class, NamedConditionConfig.class)
|
||||
.run(context -> assertThat(context.getBean(RoutingCondition.class)
|
||||
.matches("other", Map.of(), null)).isTrue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void invokesOrdoEventListenerBeans() {
|
||||
withDataSourceRunner.withUserConfiguration(RecordingListenerConfig.class)
|
||||
@@ -224,6 +279,46 @@ class OrdoJdbcAutoConfigurationTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class NamedActionConfig {
|
||||
@Bean
|
||||
RecordingNamedAction leaveMailAction() {
|
||||
return new RecordingNamedAction();
|
||||
}
|
||||
}
|
||||
|
||||
static class RecordingNamedAction implements NamedAction {
|
||||
final List<String> executed = new java.util.concurrent.CopyOnWriteArrayList<>();
|
||||
|
||||
@Override
|
||||
public String key() {
|
||||
return "leave-mail";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(ProcessRuntime runtime) {
|
||||
executed.add(key());
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class NamedConditionConfig {
|
||||
@Bean
|
||||
NamedCondition amountGt() {
|
||||
return new NamedCondition() {
|
||||
@Override
|
||||
public String key() {
|
||||
return "amountGt";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(Map<String, Object> args, ProcessRuntime runtime) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class RecordingListenerConfig {
|
||||
@Bean
|
||||
|
||||
+98
@@ -1,6 +1,10 @@
|
||||
package com.jetlumen.ordo.spring.rest;
|
||||
|
||||
import com.jetlumen.ordo.api.NamedAction;
|
||||
import com.jetlumen.ordo.api.NamedCondition;
|
||||
import com.jetlumen.ordo.api.OrdoCatalog;
|
||||
import com.jetlumen.ordo.api.OrdoEngine;
|
||||
import com.jetlumen.ordo.api.ProcessRuntime;
|
||||
import com.jetlumen.ordo.spring.jdbc.OrdoFlywayAutoConfiguration;
|
||||
import com.jetlumen.ordo.spring.jdbc.OrdoJdbcAutoConfiguration;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -12,11 +16,14 @@ import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
|
||||
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
@@ -76,6 +83,37 @@ class OrdoRestAutoConfigurationTest {
|
||||
.hasSingleBean(OrdoCatalogController.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void projectsCatalogFromNamedHandlers() {
|
||||
runner.withPropertyValues("ordo.rest.enabled=true")
|
||||
.withUserConfiguration(NamedCatalogConfig.class)
|
||||
.run(context -> {
|
||||
MockMvc mvc = MockMvcBuilders.webAppContextSetup(context).build();
|
||||
mvc.perform(get("/ordo/catalog/actions"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$[0].key").value("leave-mail"))
|
||||
.andExpect(jsonPath("$[0].label").value("Notify HR"));
|
||||
mvc.perform(get("/ordo/catalog/conditions"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$[0].key").value("amountGt"));
|
||||
mvc.perform(get("/ordo/catalog/assignees"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$").isEmpty());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void honoursUserDefinedCatalog() {
|
||||
runner.withPropertyValues("ordo.rest.enabled=true")
|
||||
.withUserConfiguration(NamedCatalogConfig.class, CustomCatalogConfig.class)
|
||||
.run(context -> {
|
||||
MockMvc mvc = MockMvcBuilders.webAppContextSetup(context).build();
|
||||
mvc.perform(get("/ordo/catalog/actions"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$[0].key").value("custom-action"));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void parsePublishStartApproveAndErrorMappings() {
|
||||
runner.withPropertyValues("ordo.rest.enabled=true").run(context -> {
|
||||
@@ -149,4 +187,64 @@ class OrdoRestAutoConfigurationTest {
|
||||
.andExpect(status().isBadRequest());
|
||||
});
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class NamedCatalogConfig {
|
||||
@Bean
|
||||
NamedAction leaveMail() {
|
||||
return new NamedAction() {
|
||||
@Override
|
||||
public String key() {
|
||||
return "leave-mail";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String label() {
|
||||
return "Notify HR";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(ProcessRuntime runtime) {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
NamedCondition amountGt() {
|
||||
return new NamedCondition() {
|
||||
@Override
|
||||
public String key() {
|
||||
return "amountGt";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(Map<String, Object> args, ProcessRuntime runtime) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class CustomCatalogConfig {
|
||||
@Bean
|
||||
OrdoCatalog ordoCatalog() {
|
||||
return new OrdoCatalog() {
|
||||
@Override
|
||||
public java.util.List<OrdoCatalog.Item> conditions() {
|
||||
return java.util.List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.List<OrdoCatalog.Item> actions() {
|
||||
return java.util.List.of(new OrdoCatalog.Item("custom-action", "Custom"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.List<OrdoCatalog.Item> assignees() {
|
||||
return java.util.List.of();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user