// 流程模型 —— LogicFlow JSON → ProcessModel(8 节点类型)
// 移植自 rust parser.rs;spec/01、spec/02
///|
pub(all) struct ProcessModel {
name : String
display_name : String
model_type : String
expire_time : String?
persist_mode : String? // ARCHIVE / SYNC
pre_interceptors : String? // 定义级前置拦截器声明(audit8 R91 补齐:逗号分隔类全名,java ProcessModel.preInterceptors 同位)
post_interceptors : String? // 定义级后置拦截器声明(逗号分隔类全名;java NodeModel.execPostInterceptors 同位)
rel_table_name : String?
nodes : Array[NodeModel]
edges : Array[EdgeModel]
} derive(Eq)
///|
pub fn ProcessModel::get_start(self : ProcessModel) -> NodeModel? {
let mut found : NodeModel? = None
for n in self.nodes {
if n.node_type is Start {
found = Some(n)
}
}
found
}
///|
pub fn ProcessModel::get_node(self : ProcessModel, id : String) -> NodeModel? {
let mut found : NodeModel? = None
for n in self.nodes {
if n.id == id {
found = Some(n)
}
}
found
}
///|
pub fn ProcessModel::get_nodes_by_type(self : ProcessModel, t : NodeType) -> Array[NodeModel] {
let out : Array[NodeModel] = []
for n in self.nodes {
if n.node_type == t {
out.push(n)
}
}
out
}
///|
pub fn ProcessModel::get_output_edges(self : ProcessModel, node_id : String) -> Array[EdgeModel] {
let out : Array[EdgeModel] = []
for e in self.edges {
if e.source_node_id == node_id {
out.push(e)
}
}
out
}
///|
pub fn ProcessModel::get_input_edges(self : ProcessModel, node_id : String) -> Array[EdgeModel] {
let out : Array[EdgeModel] = []
for e in self.edges {
if e.target_node_id == node_id {
out.push(e)
}
}
out
}
///|
pub fn ProcessModel::get_target_node(self : ProcessModel, edge : EdgeModel) -> NodeModel? {
self.get_node(edge.target_node_id)
}
///|
/// start 后第一个任务节点
pub fn ProcessModel::get_first_task_node(self : ProcessModel) -> NodeModel? {
// spec/06:710:首个 `snaker:task` 节点按 nodes 序取,不跟 start 首条出边
// (首边到 fork/decision 时旧实现会把非任务节点当首任务)。对齐 java/csharp。
for n in self.nodes {
if n.node_type is Task {
return Some(n)
}
}
None
}
///|
pub fn ProcessModel::is_first_task_node(self : ProcessModel, node_id : String) -> Bool {
match self.get_first_task_node() {
Some(first) => first.id == node_id
None => false
}
}
/// 能否退回到 parent_id 所在节点——照 mldong-boot2 NodeModel.canRejected 的形状:
/// 自 current 的入边回溯,命中 parent 放行;入边来源是 fork/join/start 时**跳过该条入边、
/// 不再深入**(boot2 是 continue,不是穿越),其余来源递归。subprocess 在 boot2 里被注释掉,
/// 等同普通节点。
pub fn ProcessModel::can_rejected(
self : ProcessModel, current_id : String, parent_id : String
) -> Bool {
for e in self.get_input_edges(current_id) {
let source = e.source_node_id
if source == parent_id {
return true
}
let skip = match self.get_node(source) {
None => true
Some(n) =>
n.node_type == Fork || n.node_type == Join || n.node_type == Start
}
if !skip && self.can_rejected(source, parent_id) {
return true
}
}
false
}
///|
pub fn ProcessModel::get_end_nodes(self : ProcessModel) -> Array[NodeModel] {
self.get_nodes_by_type(End)
}
///|
/// 声明串归一:缺失/非串/纯空白(含 `"postInterceptors": ""` 这把共享夹具里常见的空声明)
/// 一律 ⇒ `None`=「没声明」。审计要点:不能把空串当声明——那会让「声明档」命中而把
/// 引擎级注册列表整条绕过去(`flows/10-mixed-mode.json` 就是两把键都写成空串的形状)。
fn normalize_interceptor_decl(s : String?) -> String? {
match s {
Some(v) => {
let t = v.trim(chars=" \t\r\n").to_owned()
if t.is_empty() {
None
} else {
Some(t)
}
}
None => None
}
}
///|
/// 按档取「本次动作生效的拦截器声明」(audit8 R91,issues/234 §4.2/spec02 §3 三条)。
/// 基准=java `NodeModel.java:49-59`:节点 `properties` 声明优先,串空才回落流程根同名键,
/// **两档不叠加**(节点声明了就不再执行根声明里的那些)。两档都没声明 ⇒ `None`,回落动作归
/// 调用方:引擎侧回落「引擎级注册列表」=R91 之前的现状语义;persist 侧把 `None` 当「没声明
/// =不挂载」。`prop` 用 java 属性名原串(`"preInterceptors"`/`"postInterceptors"`),
/// 它同时是错误文案里的档位名。本栈模型已在 `exec.process_model` 里 ⇒ 无 DB 回查,
/// 不存在 go/py/node 那条「根声明每定义只回查一行」的读放大问题。
pub fn ProcessModel::interceptor_decl(
self : ProcessModel, node : NodeModel?, prop : String
) -> String? {
let node_decl = match node {
Some(n) => n.prop_str(prop)
None => None
}
let root_decl = if prop == "preInterceptors" {
self.pre_interceptors
} else {
self.post_interceptors
}
match normalize_interceptor_decl(node_decl) {
Some(v) => Some(v)
None => normalize_interceptor_decl(root_decl)
}
}
///|
/// 8 节点类型 + 未知档(issues/142 A 批)
/// ⚠️ `Unknown` **不是第 9 种节点**,它是「类型表里没查到」那一档。旧形状是 `_ => Custom` 兜底
/// ⇒ 任何未知类型(拼错大小写的 `snaker:subProcess`、设计器写坏的串)都被当**记录类**执行。
/// 记录类与任务类从此分叉(spec/02 §6.1)之后,「未知一律按记录类」等于把没认出来的节点
/// 静默变成一条留痕行;反过来「未知一律报错」又违反 spec/04「节点属性配错不该把流程炸掉」。
/// ⇒ 本档只做两件事:**先记一条可诊断日志(带节点 id 与实得类型串),再跳过该节点**,
/// 逐字照 java `ModelParser.java:79-90`(WARNING 后 continue)+ spec/02 类型键义务 2。
/// 注:大小写归一与 `subProcess`/无前缀别名是 issues/141 G4 的 C 方案(先立法后补实现),
/// 本档**不做归一化** ⇒ 照 spec 写无前缀 `task` 的定义会落进这里,那是义务 1 未落地的已知后果。
pub(all) enum NodeType {
Start
Task
Decision
Fork
Join
End
Custom
SubProcess
Unknown
} derive(Eq, Debug)
///|
pub fn NodeType::from_snaker_type(t : String) -> NodeType {
match t {
"snaker:start" => Start
"snaker:task" => Task
"snaker:decision" => Decision
"snaker:fork" => Fork
"snaker:join" => Join
"snaker:end" => End
"snaker:subprocess" => SubProcess
// issues/142 A 批:custom 从此是**显式档**,不再由兜底臂代答——旧兜底臂把「没认出来的
// 类型」和「记录类节点」塌成同一档,拼错的定义会被当记录类照常执行并落一条留痕行。
"snaker:custom" => Custom
_ => Unknown
}
}
///|
pub fn NodeType::as_str(self : NodeType) -> String {
match self {
Start => "start"
Task => "task"
Decision => "decision"
Fork => "fork"
Join => "join"
End => "end"
Custom => "custom"
SubProcess => "subprocess"
Unknown => "unknown"
}
}
///|
/// 未知类型档的诊断日志文案(spec/02 类型键义务 2「带节点 id 与实得类型串」)。
/// 抽成纯函数有两个理由:① 这句文案是跨栈对得上的契约面(java/php 都是「该节点及其出边将被跳过:
/// nodeId=.., type=..」那一形),钉住文案才谈得上「可诊断」;② `println` 的效果在测试里读不回来,
/// 判据留在纯函数、副作用留在调用点,两件事各自可测。
pub fn unknown_node_warning(node_id : String, raw_type : String) -> String {
"[jeeflow] 流程定义里的节点类型不在类型表里,该节点及其出边将被跳过: nodeId=\{node_id}, type=\{raw_type}"
}
///|
pub(all) struct NodeModel {
id : String
node_type : NodeType
display_name : String
properties : Map[String, Json]
} derive(Eq)
///|
pub fn NodeModel::prop_str(self : NodeModel, key : String) -> String? {
match self.properties.get(key) {
Some(v) => @json.as_str(v)
None => None
}
}
///|
pub fn NodeModel::prop_i64(self : NodeModel, key : String) -> Int64? {
match self.properties.get(key) {
Some(v) => @json.as_i64(v)
None => None
}
}
///|
/// 固定参与人
pub fn NodeModel::assignee(self : NodeModel) -> String? {
self.prop_str("assignee")
}
///|
/// 动态参与人 handler 名
pub fn NodeModel::assignment_handler(self : NodeModel) -> String? {
self.prop_str("assignmentHandler")
}
///|
/// 会签类型(C4 容错:数字 1/'1'/'ALL'/'COUNTERSIGN' 均认会签,其余 0)
pub fn NodeModel::perform_type(self : NodeModel) -> Int {
match self.prop_i64("performType") {
Some(n) => if n == 1L { 1 } else { 0 }
None =>
match self.prop_str("performType") {
Some(s) => {
let upper = s.to_upper()
if upper == "1" || upper == "ALL" || upper == "COUNTERSIGN" {
1
} else {
try @string.parse_int(s) catch {
_ => 0
} noraise {
n => if n == 1 { 1 } else { 0 }
}
}
}
None => 0
}
}
}
///|
pub fn NodeModel::task_type(self : NodeModel) -> Int {
match self.prop_i64("taskType") {
Some(n) => n.to_int()
None => 0
}
}
///|
pub fn NodeModel::countersign_type(self : NodeModel) -> String {
match self.prop_str("countersignType") {
Some(s) => s
None => "PARALLEL"
}
}
///|
/// 会签完成条件(顶层属性优先,field 子对象兜底)
pub fn NodeModel::countersign_completion_condition(self : NodeModel) -> String? {
match self.prop_str("countersignCompletionCondition") {
Some(c) => return Some(c)
None => ()
}
match self.properties.get("field") {
Some(field_val) =>
match field_val {
Json::Object(field_obj) =>
for k, v in field_obj {
if k == "countersignCompletionCondition" {
return @json.as_str(v)
}
}
_ => ()
}
None => ()
}
None
}
///|
pub fn NodeModel::expr(self : NodeModel) -> String? {
self.prop_str("expr")
}
///|
pub fn NodeModel::form_key(self : NodeModel) -> String? {
self.prop_str("form")
}
///|
/// 顶层优先、`field` 子对象兜底取串(issues/177 R11:候选人双源,形状同会签条件腿)
fn NodeModel::prop_str_dual(self : NodeModel, key : String) -> String? {
match self.prop_str(key) {
Some(s) => return Some(s)
None => ()
}
match self.properties.get("field") {
Some(field_val) =>
match field_val {
Json::Object(field_obj) =>
for k, v in field_obj {
if k == key {
return @json.as_str(v)
}
}
_ => ()
}
None => ()
}
None
}
///|
pub fn NodeModel::candidate_users(self : NodeModel) -> String? {
self.prop_str_dual("candidateUsers")
}
///|
pub fn NodeModel::candidate_groups(self : NodeModel) -> String? {
self.prop_str_dual("candidateGroups")
}
///|
/// 字段权限(PERMISSION_* → 1 只读/2 可编辑/3 隐藏,C19)
pub fn NodeModel::field_permissions(self : NodeModel) -> Map[String, Int] {
let perms : Map[String, Int] = Map([])
for k, v in self.properties {
if k.has_prefix("PERMISSION_") {
match @json.as_i64(v) {
Some(n) => perms[k] = n.to_int()
None => ()
}
}
}
perms
}
///|
pub fn NodeModel::sub_process_name(self : NodeModel) -> String? {
self.prop_str("subProcessName")
}
///|
pub fn NodeModel::is_countersign(self : NodeModel) -> Bool {
self.perform_type() == 1
}
///|
pub(all) struct EdgeModel {
id : String
source_node_id : String
target_node_id : String
label : String?
properties : Map[String, Json]
} derive(Eq)
///|
pub fn EdgeModel::expr(self : EdgeModel) -> String? {
match self.properties.get("expr") {
Some(v) => @json.as_str(v)
None => None
}
}
///|
pub fn EdgeModel::source_node_id(self : EdgeModel) -> String {
self.source_node_id
}
///|
pub fn EdgeModel::target_node_id(self : EdgeModel) -> String {
self.target_node_id
}
///|
/// LogicFlow JSON → ProcessModel
pub fn parse_model(json_str : String) -> ProcessModel raise @error.JeeflowError {
let root = @json.parse_json(json_str)
let name = match @json.get_str(root, "name") {
Some(s) => s
None => "unknown"
}
let display_name = @json.get_str(root, "displayName").unwrap_or(name)
let model_type = @json.get_str(root, "type").unwrap_or("approval")
let expire_time = @json.get_str(root, "expireTime")
let persist_mode = @json.get_str(root, "persistMode")
// 定义级后置拦截器声明(issues/150 的②要它才安全:java 是"声明即挂载",节点级缺省回落流程级)
// 定义级前置拦截器声明(audit8 R91:节点未声明时的回落档;键形=java 类全名串,与 post 同形)
let pre_interceptors = @json.get_str(root, "preInterceptors")
let post_interceptors = @json.get_str(root, "postInterceptors")
let rel_table_name = match @json.get_str(root, "relTableName") {
Some(s) => Some(s)
None => @json.get_str(root, "name")
}
// nodes
let nodes : Array[NodeModel] = []
match @json.get(root, "nodes") {
Some(nodes_val) =>
match nodes_val {
Json::Array(items) =>
for node_val in items {
let id = @json.get_str(node_val, "id").unwrap_or("")
let raw_type = @json.get_str(node_val, "type").unwrap_or("snaker:task")
let node_type = NodeType::from_snaker_type(raw_type)
// issues/142 A 批 · spec/02 类型键义务 2:**先留可诊断日志,再跳过**该节点。
// 节点不入模型 ⇒ 它的出边随后取不到目标,与 java ModelParser 的 WARNING+continue 同形;
// 旧形状是 `_ => Custom` 把未知档当记录类照常执行(本轮把两件事分开)。
if node_type is Unknown {
println(unknown_node_warning(id, raw_type))
continue
}
let display_name = match @json.get_str(node_val, "displayName") {
Some(s) => Some(s)
None =>
match @json.get(node_val, "text") {
Some(t) => @json.get_str(t, "value")
None => None
}
}
let properties : Map[String, Json] = Map([])
match @json.get(node_val, "properties") {
Some(props) =>
match props {
Json::Object(m) =>
for k, v in m {
properties[k] = v
}
_ => ()
}
None => ()
}
nodes.push({ id, node_type, display_name: display_name.unwrap_or(""), properties })
}
_ => ()
}
None => ()
}
// edges
let edges : Array[EdgeModel] = []
match @json.get(root, "edges") {
Some(edges_val) =>
match edges_val {
Json::Array(items) =>
for edge_val in items {
let id = @json.get_str(edge_val, "id").unwrap_or("")
let source_node_id = @json.get_str(edge_val, "sourceNodeId").unwrap_or("")
let target_node_id = @json.get_str(edge_val, "targetNodeId").unwrap_or("")
let label = match @json.get(edge_val, "text") {
Some(t) => @json.get_str(t, "value")
None => None
}
let properties : Map[String, Json] = Map([])
match @json.get(edge_val, "properties") {
Some(props) =>
match props {
Json::Object(m) =>
for k, v in m {
properties[k] = v
}
_ => ()
}
None => ()
}
edges.push({ id, source_node_id, target_node_id, label, properties })
}
_ => ()
}
None => ()
}
{ name, display_name, model_type, expire_time, persist_mode, pre_interceptors, post_interceptors, rel_table_name, nodes, edges }
}
// ── 显式 extend(implicit_impl_as_method 弃办:impl 的方法不再被隐式提升为常规方法)──
// 由 scripts/moon_warn_w2.py 从编译器消息里生成;一行一个 (类型, trait),可 diff 可复审。
pub extend ProcessModel with Eq::{equal, not_equal}
pub extend NodeType with Eq::{equal, not_equal}
pub extend NodeType with Debug::{to_repr}
pub extend NodeModel with Eq::{equal, not_equal}
pub extend EdgeModel with Eq::{equal, not_equal}