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:
@@ -21,14 +21,14 @@
|
|||||||
- 线性或多步图:`StepTransition` + 内置谓词 / 宿主 `RoutingCondition`
|
- 线性或多步图:`StepTransition` + 内置谓词 / 宿主 `RoutingCondition`
|
||||||
- 会签/或签:`ApprovalPolicy.ALL` / `ANY`(多候选人)
|
- 会签/或签:`ApprovalPolicy.ALL` / `ANY`(多候选人)
|
||||||
- 结构化并行:`kind: PARALLEL` 块(一层、join ALL)
|
- 结构化并行:`kind: PARALLEL` 块(一层、join ALL)
|
||||||
- ACTION 步骤:事务提交后调用宿主 `ActionHandler`
|
- ACTION 步骤:事务提交后调用宿主 `ActionHandler`(可经 `NamedAction` 分发)
|
||||||
- 发起人撤回:`WITHDRAWN`,待办任务 `SKIPPED`
|
- 发起人撤回:`WITHDRAWN`,待办任务 `SKIPPED`
|
||||||
- 管理员/系统取消:`cancel` → `CANCELLED`,待办任务 `SKIPPED`(引擎不鉴权角色)
|
- 管理员/系统取消:`cancel` → `CANCELLED`,待办任务 `SKIPPED`(引擎不鉴权角色)
|
||||||
- 任务转派:当前办理人 `reassign`,审计 `TASK_REASSIGNED`
|
- 任务转派:当前办理人 `reassign`,审计 `TASK_REASSIGNED`
|
||||||
- 到期升级:步骤 `due` + `processDue`,审计 `TASK_ESCALATED`
|
- 到期升级:步骤 `due` + `processDue`,审计 `TASK_ESCALATED`
|
||||||
- 分页查询:任务 / 实例 / 流程定义
|
- 分页查询:任务 / 实例 / 流程定义
|
||||||
- 审计时间线:`ProcessEvent` + `queryHistory`
|
- 审计时间线:`ProcessEvent` + `queryHistory`
|
||||||
- 扩展点:`AssigneeResolver`、`RoutingCondition`、`ActionHandler`、`OrdoEventListener`
|
- 扩展点:`AssigneeResolver`、`RoutingCondition`、`ActionHandler`、`NamedCondition`、`NamedAction`、`OrdoEventListener`
|
||||||
|
|
||||||
开发计划:可选 REST + 目录 SPI。设计器为独立产品(不进本仓库),待 REST 与目录之后。多租户 **暂不在计划中**。见 [docs/roadmap.md](docs/roadmap.md)。
|
开发计划:可选 REST + 目录 SPI。设计器为独立产品(不进本仓库),待 REST 与目录之后。多租户 **暂不在计划中**。见 [docs/roadmap.md](docs/roadmap.md)。
|
||||||
|
|
||||||
@@ -112,8 +112,9 @@ ordo:
|
|||||||
| Bean | 默认 |
|
| Bean | 默认 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `AssigneeResolver` | 候选人即办理人 |
|
| `AssigneeResolver` | 候选人即办理人 |
|
||||||
| `RoutingCondition` | 始终匹配(`ref` 边;宿主可按 key+args 分发) |
|
| `NamedCondition` / `NamedAction` | 可多个;无对应门面时按 key 分发 |
|
||||||
| `ActionHandler` | 空操作 |
|
| `RoutingCondition` | 无具名 condition 时始终匹配;有则未知 `ref` 为 false |
|
||||||
|
| `ActionHandler` | 无具名 action 时空操作;有则未知 key 抛错 |
|
||||||
| `OrdoEventListener` | 可注册多个,提交后按顺序调用 |
|
| `OrdoEventListener` | 可注册多个,提交后按顺序调用 |
|
||||||
|
|
||||||
## 运行时约定
|
## 运行时约定
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
- 到期升级 `processDue` / `TASK_ESCALATED`
|
- 到期升级 `processDue` / `TASK_ESCALATED`
|
||||||
- 管理员/系统取消 `cancel` / `CANCELLED`
|
- 管理员/系统取消 `cancel` / `CANCELLED`
|
||||||
- 可选 REST(autoconfigure 条件装配)+ 目录 SPI `OrdoCatalog`
|
- 可选 REST(autoconfigure 条件装配)+ 目录 SPI `OrdoCatalog`
|
||||||
|
- `NamedAction` / `NamedCondition` 注册与官方 key 分发;默认 Catalog 从具名 Bean 投影
|
||||||
|
|
||||||
## 开发计划(确定要做)
|
## 开发计划(确定要做)
|
||||||
|
|
||||||
|
|||||||
+21
-14
@@ -76,12 +76,16 @@ ordo:
|
|||||||
| Bean | 默认 |
|
| Bean | 默认 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `AssigneeResolver` | 候选人字符串即办理人 |
|
| `AssigneeResolver` | 候选人字符串即办理人 |
|
||||||
| `RoutingCondition` | `ref` 边默认恒 true;无 `when` 的边不经过该 Bean |
|
| `NamedCondition` | 可有多个;无 `RoutingCondition` 门面且列表非空时装配 `DispatchingRoutingCondition` |
|
||||||
| `ActionHandler` | 空操作 |
|
| `NamedAction` | 可有多个;无 `ActionHandler` 门面且列表非空时装配 `DispatchingActionHandler` |
|
||||||
|
| `RoutingCondition` | 无 `NamedCondition` 时 `ref` 边恒 true;有则按 key 分发,未知 `ref` 为 false |
|
||||||
|
| `ActionHandler` | 无 `NamedAction` 时空操作;有则按 key 分发,未知 key 抛 `IllegalArgumentException` |
|
||||||
| `OrdoEventListener` | 可有多个,提交后按 Spring 顺序调用 |
|
| `OrdoEventListener` | 可有多个,提交后按 Spring 顺序调用 |
|
||||||
| `OrdoCatalog` | 空列表(仅 REST 打开时装配) |
|
| `OrdoCatalog` | REST 打开时:从 `NamedCondition` / `NamedAction` 投影;皆空则为空列表。`assignees` 仍空 |
|
||||||
|
|
||||||
未提供自定义 Bean 时,ACTION 步骤仍会推进流程,但 handler 什么都不做。
|
未提供 `NamedAction` 且未覆盖 `ActionHandler` 时,ACTION 步骤仍会推进流程,但 handler 什么都不做。不要同时提供门面 Bean 与对应 `Named*`(门面优先,具名 Bean 不参与运行时)。
|
||||||
|
|
||||||
|
内存引擎把 `DispatchingRoutingCondition.of(...)` / `DispatchingActionHandler.of(...)` 传入 `InMemoryOrdoEngine` 即可。
|
||||||
|
|
||||||
### 2.3 可选 REST
|
### 2.3 可选 REST
|
||||||
|
|
||||||
@@ -294,9 +298,9 @@ v1:至少 2 条分支;禁止套娃 PARALLEL;join 固定 ALL;任一分支
|
|||||||
|
|
||||||
无条件边是默认分支;`priority` 只在同类边之间比较(多条条件边之间,或多条无条件边之间)。
|
无条件边是默认分支;`priority` 只在同类边之间比较(多条条件边之间,或多条无条件边之间)。
|
||||||
|
|
||||||
`RoutingCondition.matches` 看到 `args` 与 `ProcessRuntime`(含业务 `context`、发起人、定义与当前步)。看不到任务意见。上下文在 `start` 时写入,运行中引擎不会改 context。未知 `ref` 由宿主返回 `false`,该边不匹配。内置谓词仍只读 `ProcessContext` 变量。
|
`RoutingCondition.matches` 看到 `args` 与 `ProcessRuntime`(含业务 `context`、发起人、定义与当前步)。看不到任务意见。上下文在 `start` 时写入,运行中引擎不会改 context。内置谓词仍只读 `ProcessContext` 变量。
|
||||||
|
|
||||||
引擎只注入**一个** `RoutingCondition`。Spring 下多个该类型 Bean 会冲突。宿主用一个门面按 `ref` 分发;不要指望引擎按定义拆 bean。
|
引擎仍只注入**一个** `RoutingCondition`。Spring 下注册多个 `NamedCondition` Bean(`key()` 对应 `when.ref`);无门面且列表非空时装配官方分发器。未知 `ref` 为 false,该边不匹配。重复 `key` 启动失败。也可继续提供单个 `RoutingCondition` 门面自行分发(不要与 `NamedCondition` 混用)。无 `NamedCondition` 时默认仍是恒 true。
|
||||||
|
|
||||||
## 7. ACTION 步骤
|
## 7. ACTION 步骤
|
||||||
|
|
||||||
@@ -307,22 +311,25 @@ v1:至少 2 条分支;禁止套娃 PARALLEL;join 固定 ALL;任一分支
|
|||||||
3. 事务提交后调用 `ActionHandler.execute(actionKey, runtime)`。
|
3. 事务提交后调用 `ActionHandler.execute(actionKey, runtime)`。
|
||||||
4. 成功 → `SUCCESS` + 事件 `ACTION_SUCCEEDED`;失败 → `FAILED`(`errorMessage`)+ `ACTION_FAILED` + 日志 WARNING。
|
4. 成功 → `SUCCESS` + 事件 `ACTION_SUCCEEDED`;失败 → `FAILED`(`errorMessage`)+ `ACTION_FAILED` + 日志 WARNING。
|
||||||
|
|
||||||
**失败不回滚已提交的审批,不阻塞后续步骤,引擎不做重试。** 宿主用 `queryActionExecutions` 或 listener 自行补发。
|
**失败不回滚已提交的审批,不阻塞后续步骤,引擎不做重试。** 宿主用 `queryActionExecutions` 或 listener 自行补发。分发器遇到未知 `actionKey` 会抛 `IllegalArgumentException`,记为该次 ACTION `FAILED`。
|
||||||
|
|
||||||
```java
|
```java
|
||||||
@Component
|
@Component
|
||||||
public class MailActions implements ActionHandler {
|
public class LeaveApprovedMail implements NamedAction {
|
||||||
@Override
|
@Override
|
||||||
public void execute(String actionKey, ProcessRuntime runtime) {
|
public String key() {
|
||||||
switch (actionKey) {
|
return "leave-approved-mail";
|
||||||
case "leave-submitted-mail" -> { /* ... */ }
|
}
|
||||||
case "leave-approved-mail" -> { /* ... */ }
|
|
||||||
default -> throw new IllegalStateException("unknown action: " + actionKey);
|
@Override
|
||||||
}
|
public void execute(ProcessRuntime runtime) {
|
||||||
|
/* ... */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
仍可提供单个 `ActionHandler` 按 key 自行分发;不要与 `NamedAction` 混用。
|
||||||
|
|
||||||
## 8. 审计与监听
|
## 8. 审计与监听
|
||||||
|
|
||||||
`queryHistory(instanceId, page)`:该实例事件,**发生时间升序**。
|
`queryHistory(instanceId, page)`:该实例事件,**发生时间升序**。
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ package com.jetlumen.ordo.api;
|
|||||||
/**
|
/**
|
||||||
* Executes a named action step against the running instance. Hosts supply a
|
* Executes a named action step against the running instance. Hosts supply a
|
||||||
* singleton implementation (same pattern as {@link RoutingCondition}); the database only stores
|
* singleton implementation (same pattern as {@link RoutingCondition}); the database only stores
|
||||||
* the {@code actionKey} string.
|
* the {@code actionKey} string. Prefer {@link NamedAction} beans; Spring wires a dispatcher
|
||||||
|
* when this facade is not defined.
|
||||||
*/
|
*/
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
public interface ActionHandler {
|
public interface ActionHandler {
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.jetlumen.ordo.api;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single ACTION implementation keyed by {@link #key()}, matching {@code action} on the step
|
||||||
|
* (and {@code due.then.notify} keys). Spring collects these beans into a dispatching
|
||||||
|
* {@link ActionHandler} when the host does not define that facade itself.
|
||||||
|
*/
|
||||||
|
public interface NamedAction {
|
||||||
|
String key();
|
||||||
|
|
||||||
|
void execute(ProcessRuntime runtime);
|
||||||
|
|
||||||
|
default String label() {
|
||||||
|
return key();
|
||||||
|
}
|
||||||
|
|
||||||
|
default List<OrdoCatalog.Param> params() {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
default OrdoCatalog.Item catalogItem() {
|
||||||
|
return new OrdoCatalog.Item(key(), label(), params());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.jetlumen.ordo.api;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single routing condition keyed by {@link #key()}, matching {@code when.ref}. Spring collects
|
||||||
|
* these beans into a dispatching {@link RoutingCondition} when the host does not define that
|
||||||
|
* facade itself.
|
||||||
|
*/
|
||||||
|
public interface NamedCondition {
|
||||||
|
String key();
|
||||||
|
|
||||||
|
boolean matches(Map<String, Object> args, ProcessRuntime runtime);
|
||||||
|
|
||||||
|
default String label() {
|
||||||
|
return key();
|
||||||
|
}
|
||||||
|
|
||||||
|
default List<OrdoCatalog.Param> params() {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
default OrdoCatalog.Item catalogItem() {
|
||||||
|
return new OrdoCatalog.Item(key(), label(), params());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,8 @@ import java.util.Map;
|
|||||||
/**
|
/**
|
||||||
* Evaluates a named routing condition ({@code when.ref}) against the running instance.
|
* Evaluates a named routing condition ({@code when.ref}) against the running instance.
|
||||||
* Hosts supply a singleton implementation (same pattern as {@link AssigneeResolver}); the
|
* Hosts supply a singleton implementation (same pattern as {@link AssigneeResolver}); the
|
||||||
* definition stores the {@code ref} key and optional {@code args}.
|
* definition stores the {@code ref} key and optional {@code args}. Prefer {@link NamedCondition}
|
||||||
|
* beans; Spring wires a dispatcher when this facade is not defined.
|
||||||
*/
|
*/
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
public interface RoutingCondition {
|
public interface RoutingCondition {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
-4
@@ -2,6 +2,8 @@ package com.jetlumen.ordo.spring.jdbc;
|
|||||||
|
|
||||||
import com.jetlumen.ordo.api.ActionHandler;
|
import com.jetlumen.ordo.api.ActionHandler;
|
||||||
import com.jetlumen.ordo.api.AssigneeResolver;
|
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.OrdoEngine;
|
||||||
import com.jetlumen.ordo.api.OrdoEventListener;
|
import com.jetlumen.ordo.api.OrdoEventListener;
|
||||||
import com.jetlumen.ordo.api.RoutingCondition;
|
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.ProcessHistoryRepository;
|
||||||
import com.jetlumen.ordo.api.repository.ProcessInstanceRepository;
|
import com.jetlumen.ordo.api.repository.ProcessInstanceRepository;
|
||||||
import com.jetlumen.ordo.core.DefaultOrdoEngine;
|
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.spring.OrdoProperties;
|
||||||
import com.jetlumen.ordo.storage.jdbc.JdbcActionExecutionRepository;
|
import com.jetlumen.ordo.storage.jdbc.JdbcActionExecutionRepository;
|
||||||
import com.jetlumen.ordo.storage.jdbc.JdbcApprovalTaskRepository;
|
import com.jetlumen.ordo.storage.jdbc.JdbcApprovalTaskRepository;
|
||||||
@@ -37,6 +41,7 @@ import org.springframework.context.annotation.Bean;
|
|||||||
|
|
||||||
import javax.sql.DataSource;
|
import javax.sql.DataSource;
|
||||||
import java.time.Clock;
|
import java.time.Clock;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Autoconfigures a JDBC-backed {@link OrdoEngine} when a {@link DataSource} bean is
|
* Autoconfigures a JDBC-backed {@link OrdoEngine} when a {@link DataSource} bean is
|
||||||
@@ -72,14 +77,16 @@ public class OrdoJdbcAutoConfiguration {
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnMissingBean
|
@ConditionalOnMissingBean
|
||||||
public RoutingCondition ordoRoutingCondition() {
|
public RoutingCondition ordoRoutingCondition(ObjectProvider<NamedCondition> namedConditions) {
|
||||||
return RoutingCondition.always();
|
List<NamedCondition> conditions = namedConditions.orderedStream().toList();
|
||||||
|
return conditions.isEmpty() ? RoutingCondition.always() : DispatchingRoutingCondition.of(conditions);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnMissingBean
|
@ConditionalOnMissingBean
|
||||||
public ActionHandler ordoActionHandler() {
|
public ActionHandler ordoActionHandler(ObjectProvider<NamedAction> namedActions) {
|
||||||
return ActionHandler.noop();
|
List<NamedAction> actions = namedActions.orderedStream().toList();
|
||||||
|
return actions.isEmpty() ? ActionHandler.noop() : DispatchingActionHandler.of(actions);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
|||||||
+14
-2
@@ -1,9 +1,13 @@
|
|||||||
package com.jetlumen.ordo.spring.rest;
|
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.OrdoCatalog;
|
||||||
import com.jetlumen.ordo.api.OrdoEngine;
|
import com.jetlumen.ordo.api.OrdoEngine;
|
||||||
|
import com.jetlumen.ordo.core.spi.RegistryOrdoCatalog;
|
||||||
import com.jetlumen.ordo.spring.OrdoProperties;
|
import com.jetlumen.ordo.spring.OrdoProperties;
|
||||||
import com.jetlumen.ordo.spring.jdbc.OrdoJdbcAutoConfiguration;
|
import com.jetlumen.ordo.spring.jdbc.OrdoJdbcAutoConfiguration;
|
||||||
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
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.context.annotation.Import;
|
||||||
import org.springframework.web.servlet.DispatcherServlet;
|
import org.springframework.web.servlet.DispatcherServlet;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@AutoConfiguration
|
@AutoConfiguration
|
||||||
@ConditionalOnClass(DispatcherServlet.class)
|
@ConditionalOnClass(DispatcherServlet.class)
|
||||||
@ConditionalOnProperty(prefix = "ordo.rest", name = "enabled", havingValue = "true")
|
@ConditionalOnProperty(prefix = "ordo.rest", name = "enabled", havingValue = "true")
|
||||||
@@ -33,7 +39,13 @@ public class OrdoRestAutoConfiguration {
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnMissingBean
|
@ConditionalOnMissingBean
|
||||||
public OrdoCatalog ordoCatalog() {
|
public OrdoCatalog ordoCatalog(ObjectProvider<NamedAction> namedActions,
|
||||||
return OrdoCatalog.empty();
|
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.ApprovalStep;
|
||||||
import com.jetlumen.ordo.api.ApprovalTask;
|
import com.jetlumen.ordo.api.ApprovalTask;
|
||||||
import com.jetlumen.ordo.api.AssigneeResolver;
|
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.OrdoEngine;
|
||||||
import com.jetlumen.ordo.api.OrdoEventListener;
|
import com.jetlumen.ordo.api.OrdoEventListener;
|
||||||
import com.jetlumen.ordo.api.ProcessDefinition;
|
import com.jetlumen.ordo.api.ProcessDefinition;
|
||||||
import com.jetlumen.ordo.api.ProcessEvent;
|
import com.jetlumen.ordo.api.ProcessEvent;
|
||||||
import com.jetlumen.ordo.api.ProcessEventType;
|
import com.jetlumen.ordo.api.ProcessEventType;
|
||||||
import com.jetlumen.ordo.api.ProcessInstance;
|
import com.jetlumen.ordo.api.ProcessInstance;
|
||||||
|
import com.jetlumen.ordo.api.ProcessRuntime;
|
||||||
import com.jetlumen.ordo.api.RoutingCondition;
|
import com.jetlumen.ordo.api.RoutingCondition;
|
||||||
|
import com.jetlumen.ordo.api.StepTransition;
|
||||||
import com.jetlumen.ordo.api.query.PageRequest;
|
import com.jetlumen.ordo.api.query.PageRequest;
|
||||||
import com.jetlumen.ordo.api.repository.ProcessDefinitionRepository;
|
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 com.jetlumen.ordo.storage.jdbc.dialect.SqlDialect;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||||
@@ -23,6 +29,7 @@ import org.springframework.context.annotation.Bean;
|
|||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
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
|
@Test
|
||||||
void invokesOrdoEventListenerBeans() {
|
void invokesOrdoEventListenerBeans() {
|
||||||
withDataSourceRunner.withUserConfiguration(RecordingListenerConfig.class)
|
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
|
@Configuration
|
||||||
static class RecordingListenerConfig {
|
static class RecordingListenerConfig {
|
||||||
@Bean
|
@Bean
|
||||||
|
|||||||
+98
@@ -1,6 +1,10 @@
|
|||||||
package com.jetlumen.ordo.spring.rest;
|
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.OrdoEngine;
|
||||||
|
import com.jetlumen.ordo.api.ProcessRuntime;
|
||||||
import com.jetlumen.ordo.spring.jdbc.OrdoFlywayAutoConfiguration;
|
import com.jetlumen.ordo.spring.jdbc.OrdoFlywayAutoConfiguration;
|
||||||
import com.jetlumen.ordo.spring.jdbc.OrdoJdbcAutoConfiguration;
|
import com.jetlumen.ordo.spring.jdbc.OrdoJdbcAutoConfiguration;
|
||||||
import org.junit.jupiter.api.Test;
|
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.DispatcherServletAutoConfiguration;
|
||||||
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
|
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
|
||||||
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
|
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.http.MediaType;
|
||||||
import org.springframework.test.web.servlet.MockMvc;
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
import org.springframework.test.web.servlet.MvcResult;
|
import org.springframework.test.web.servlet.MvcResult;
|
||||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import com.jayway.jsonpath.JsonPath;
|
import com.jayway.jsonpath.JsonPath;
|
||||||
@@ -76,6 +83,37 @@ class OrdoRestAutoConfigurationTest {
|
|||||||
.hasSingleBean(OrdoCatalogController.class));
|
.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
|
@Test
|
||||||
void parsePublishStartApproveAndErrorMappings() {
|
void parsePublishStartApproveAndErrorMappings() {
|
||||||
runner.withPropertyValues("ordo.rest.enabled=true").run(context -> {
|
runner.withPropertyValues("ordo.rest.enabled=true").run(context -> {
|
||||||
@@ -149,4 +187,64 @@ class OrdoRestAutoConfigurationTest {
|
|||||||
.andExpect(status().isBadRequest());
|
.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