feat(flows): 新增流程编辑器基础功能与相关组件
feat(backend): 添加流程模型与服务支持 feat(frontend): 实现流程编辑器UI与交互 feat(assets): 添加流程节点图标资源 feat(plugins): 实现上下文菜单和运行时插件 feat(components): 新增基础节点和侧边栏组件 feat(routes): 添加流程相关路由配置 feat(models): 创建流程和运行日志数据模型 feat(services): 实现流程服务层逻辑 feat(migration): 添加流程相关数据库迁移 feat(config): 更新前端配置支持流程编辑器 feat(utils): 增强axios错误处理和工具函数
This commit is contained in:
438
backend/src/services/flow_service.rs
Normal file
438
backend/src/services/flow_service.rs
Normal file
@ -0,0 +1,438 @@
|
||||
// removed unused: use std::collections::HashMap;
|
||||
// removed unused: use std::sync::Mutex;
|
||||
use anyhow::Context as _;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::AppError;
|
||||
use crate::flow::{self, dsl::FlowDSL, engine::FlowEngine, context::{DriveOptions, ExecutionMode}};
|
||||
use crate::db::Db;
|
||||
use crate::models::flow as db_flow;
|
||||
use crate::models::request_log; // 新增:查询最近修改人
|
||||
use crate::services::flow_run_log_service;
|
||||
use crate::services::flow_run_log_service::CreateRunLogInput;
|
||||
use sea_orm::{EntityTrait, ActiveModelTrait, Set, DbErr, ColumnTrait, QueryFilter, PaginatorTrait, QueryOrder};
|
||||
use sea_orm::entity::prelude::DateTimeWithTimeZone; // 新增:时间类型
|
||||
use chrono::{Utc, FixedOffset};
|
||||
use tracing::{info, error};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FlowSummary {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")] pub code: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")] pub remark: Option<String>,
|
||||
pub created_at: DateTimeWithTimeZone,
|
||||
pub updated_at: DateTimeWithTimeZone,
|
||||
#[serde(skip_serializing_if = "Option::is_none")] pub last_modified_by: Option<String>,
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FlowDoc { pub id: String, pub yaml: String, #[serde(skip_serializing_if = "Option::is_none")] pub design_json: Option<serde_json::Value> }
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FlowCreateReq { pub yaml: Option<String>, pub name: Option<String>, pub design_json: Option<serde_json::Value>, pub code: Option<String>, pub remark: Option<String> }
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FlowUpdateReq { pub yaml: Option<String>, pub design_json: Option<serde_json::Value>, pub name: Option<String>, pub code: Option<String>, pub remark: Option<String> }
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RunReq { #[serde(default)] pub input: serde_json::Value }
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RunResult { pub ok: bool, pub ctx: serde_json::Value, pub logs: Vec<String> }
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize)]
|
||||
pub struct PageResp<T> { pub items: Vec<T>, pub total: u64, pub page: u64, pub page_size: u64 }
|
||||
|
||||
// list flows from database with pagination & keyword
|
||||
pub async fn list(db: &Db, page: u64, page_size: u64, keyword: Option<String>) -> anyhow::Result<PageResp<FlowSummary>> {
|
||||
let mut selector = db_flow::Entity::find();
|
||||
if let Some(k) = keyword.filter(|s| !s.is_empty()) {
|
||||
let like = format!("%{}%", k);
|
||||
selector = selector.filter(
|
||||
db_flow::Column::Name.like(like.clone())
|
||||
.or(db_flow::Column::Id.like(like))
|
||||
);
|
||||
}
|
||||
let paginator = selector.order_by_desc(db_flow::Column::CreatedAt).paginate(db, page_size);
|
||||
let total = paginator.num_items().await? as u64;
|
||||
let models = paginator.fetch_page(if page > 0 { page - 1 } else { 0 }).await?;
|
||||
let mut items: Vec<FlowSummary> = Vec::with_capacity(models.len());
|
||||
for row in models.into_iter() {
|
||||
let id = row.id.clone();
|
||||
let name = row
|
||||
.name
|
||||
.clone()
|
||||
.or_else(|| row.yaml.as_deref().and_then(extract_name))
|
||||
.unwrap_or_else(|| {
|
||||
let prefix: String = id.chars().take(8).collect();
|
||||
format!("flow_{}", prefix)
|
||||
});
|
||||
// 最近修改人:从请求日志中查找最近一次对该flow的PUT请求
|
||||
let last_modified_by = request_log::Entity::find()
|
||||
.filter(request_log::Column::Path.like(format!("/api/flows/{}%", id)))
|
||||
.filter(request_log::Column::Method.eq("PUT"))
|
||||
.order_by_desc(request_log::Column::RequestTime)
|
||||
.one(db)
|
||||
.await?
|
||||
.and_then(|m| m.username);
|
||||
items.push(FlowSummary {
|
||||
id,
|
||||
name,
|
||||
code: row.code.clone(),
|
||||
remark: row.remark.clone(),
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at,
|
||||
last_modified_by,
|
||||
});
|
||||
}
|
||||
Ok(PageResp { items, total, page, page_size })
|
||||
}
|
||||
|
||||
// create new flow with yaml or just name
|
||||
pub async fn create(db: &Db, req: FlowCreateReq) -> anyhow::Result<FlowDoc> {
|
||||
info!(target: "udmin", "flow.create: start");
|
||||
if let Some(yaml) = &req.yaml {
|
||||
let _parsed: FlowDSL = serde_yaml::from_str(yaml).context("invalid flow yaml")?;
|
||||
info!(target: "udmin", "flow.create: yaml parsed ok");
|
||||
}
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
let name = req
|
||||
.name
|
||||
.clone()
|
||||
.or_else(|| req.yaml.as_deref().and_then(extract_name));
|
||||
let now = Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap());
|
||||
let design_json_str = match &req.design_json { Some(v) => serde_json::to_string(v).ok(), None => None };
|
||||
let am = db_flow::ActiveModel {
|
||||
id: Set(id.clone()),
|
||||
name: Set(name),
|
||||
yaml: Set(req.yaml.clone()),
|
||||
design_json: Set(design_json_str),
|
||||
// 新增: code 与 remark 入库
|
||||
code: Set(req.code.clone()),
|
||||
remark: Set(req.remark.clone()),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap())),
|
||||
..Default::default()
|
||||
};
|
||||
info!(target: "udmin", "flow.create: inserting into db id={}", id);
|
||||
// Use exec() instead of insert() returning Model to avoid RecordNotInserted on non-AI PK
|
||||
match db_flow::Entity::insert(am).exec(db).await {
|
||||
Ok(_) => {
|
||||
info!(target: "udmin", "flow.create: insert ok id={}", id);
|
||||
Ok(FlowDoc { id, yaml: req.yaml.unwrap_or_default(), design_json: req.design_json })
|
||||
}
|
||||
Err(DbErr::RecordNotInserted) => {
|
||||
// Workaround for MySQL + non-auto-increment PK: verify by reading back
|
||||
error!(target: "udmin", "flow.create: insert returned RecordNotInserted, verifying by select id={}", id);
|
||||
match db_flow::Entity::find_by_id(id.clone()).one(db).await {
|
||||
Ok(Some(_)) => {
|
||||
info!(target: "udmin", "flow.create: found inserted row by id={}, treating as success", id);
|
||||
Ok(FlowDoc { id, yaml: req.yaml.unwrap_or_default(), design_json: req.design_json })
|
||||
}
|
||||
Ok(None) => Err(anyhow::anyhow!("insert flow failed").context("verify inserted row not found")),
|
||||
Err(e) => Err(anyhow::Error::new(e).context("insert flow failed")),
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!(target: "udmin", error = ?e, "flow.create: insert failed");
|
||||
Err(anyhow::Error::new(e).context("insert flow failed"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get(db: &Db, id: &str) -> anyhow::Result<FlowDoc> {
|
||||
let row = db_flow::Entity::find_by_id(id.to_string()).one(db).await?;
|
||||
let row = row.ok_or_else(|| anyhow::anyhow!("not found"))?;
|
||||
let yaml = row.yaml.unwrap_or_default();
|
||||
let design_json = row.design_json.and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok());
|
||||
Ok(FlowDoc { id: row.id, yaml, design_json })
|
||||
}
|
||||
|
||||
pub async fn update(db: &Db, id: &str, req: FlowUpdateReq) -> anyhow::Result<FlowDoc> {
|
||||
if let Some(yaml) = &req.yaml {
|
||||
let _parsed: FlowDSL = serde_yaml::from_str(yaml).context("invalid flow yaml")?;
|
||||
}
|
||||
let row = db_flow::Entity::find_by_id(id.to_string()).one(db).await?;
|
||||
let Some(row) = row else { return Err(anyhow::anyhow!("not found")); };
|
||||
let mut am: db_flow::ActiveModel = row.into();
|
||||
|
||||
if let Some(yaml) = req.yaml {
|
||||
let next_name = req
|
||||
.name
|
||||
.or_else(|| extract_name(&yaml));
|
||||
if let Some(n) = next_name { am.name = Set(Some(n)); }
|
||||
am.yaml = Set(Some(yaml.clone()));
|
||||
} else if let Some(n) = req.name { am.name = Set(Some(n)); }
|
||||
|
||||
if let Some(dj) = req.design_json {
|
||||
let s = serde_json::to_string(&dj)?;
|
||||
am.design_json = Set(Some(s));
|
||||
}
|
||||
if let Some(c) = req.code { am.code = Set(Some(c)); }
|
||||
if let Some(r) = req.remark { am.remark = Set(Some(r)); }
|
||||
|
||||
// update timestamp
|
||||
am.updated_at = Set(Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap()));
|
||||
|
||||
am.update(db).await?;
|
||||
// return latest yaml
|
||||
let got = db_flow::Entity::find_by_id(id.to_string()).one(db).await?.unwrap();
|
||||
let dj = got.design_json.as_deref().and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok());
|
||||
Ok(FlowDoc { id: id.to_string(), yaml: got.yaml.unwrap_or_default(), design_json: dj })
|
||||
}
|
||||
|
||||
pub async fn delete(db: &Db, id: &str) -> anyhow::Result<()> {
|
||||
let row = db_flow::Entity::find_by_id(id.to_string()).one(db).await?;
|
||||
let Some(row) = row else { return Err(anyhow::anyhow!("not found")); };
|
||||
let am: db_flow::ActiveModel = row.into();
|
||||
am.delete(db).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn run(db: &Db, id: &str, req: RunReq, operator: Option<(i64, String)>) -> anyhow::Result<RunResult> {
|
||||
info!(target = "udmin", "flow.run: start id={}", id);
|
||||
let start = Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap());
|
||||
// 获取流程编码,便于写入运行日志
|
||||
let flow_code: Option<String> = match db_flow::Entity::find_by_id(id.to_string()).one(db).await {
|
||||
Ok(Some(row)) => row.code,
|
||||
_ => None,
|
||||
};
|
||||
// 获取流程文档并记录失败原因
|
||||
let doc = match get(db, id).await {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
error!(target = "udmin", error = ?e, "flow.run: get doc failed id={}", id);
|
||||
// 记录失败日志
|
||||
let (user_id, username) = operator.map(|(u, n)| (Some(u), Some(n))).unwrap_or((None, None));
|
||||
let _ = flow_run_log_service::create(db, CreateRunLogInput {
|
||||
flow_id: id.to_string(),
|
||||
flow_code: flow_code.clone(),
|
||||
input: Some(serde_json::to_string(&req.input).unwrap_or_default()),
|
||||
output: None,
|
||||
ok: false,
|
||||
logs: Some(format!("get doc failed: {}", e)),
|
||||
user_id,
|
||||
username,
|
||||
started_at: start,
|
||||
duration_ms: 0,
|
||||
}).await;
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
// 记录文档基本信息,便于判断走 JSON 还是 YAML
|
||||
info!(target = "udmin", "flow.run: doc loaded id={} has_design_json={} yaml_len={}", id, doc.design_json.is_some(), doc.yaml.len());
|
||||
|
||||
// Prefer design_json if present; otherwise fall back to YAML
|
||||
let mut exec_mode: ExecutionMode = ExecutionMode::Sync;
|
||||
let (mut chain, mut ctx) = if let Some(design) = &doc.design_json {
|
||||
info!(target = "udmin", "flow.run: building chain from design_json id={}", id);
|
||||
let chain_from_json = match flow::dsl::chain_from_design_json(design) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!(target = "udmin", error = ?e, "flow.run: build chain from design_json failed id={}", id);
|
||||
// 记录失败日志
|
||||
let (user_id, username) = operator.as_ref().map(|(u, n)| (Some(*u), Some(n.clone()))).unwrap_or((None, None));
|
||||
let _ = flow_run_log_service::create(db, CreateRunLogInput {
|
||||
flow_id: id.to_string(),
|
||||
flow_code: flow_code.clone(),
|
||||
input: Some(serde_json::to_string(&req.input).unwrap_or_default()),
|
||||
output: None,
|
||||
ok: false,
|
||||
logs: Some(format!("build chain from design_json failed: {}", e)),
|
||||
user_id,
|
||||
username,
|
||||
started_at: start,
|
||||
duration_ms: 0,
|
||||
}).await;
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
let mut ctx = req.input.clone();
|
||||
// Merge node-scoped configs into ctx under ctx.nodes
|
||||
let supplement = flow::dsl::ctx_from_design_json(design);
|
||||
merge_json(&mut ctx, &supplement);
|
||||
// 解析 executionMode / execution_mode
|
||||
let mode_str = design.get("executionMode").and_then(|v| v.as_str())
|
||||
.or_else(|| design.get("execution_mode").and_then(|v| v.as_str()))
|
||||
.unwrap_or("sync");
|
||||
exec_mode = parse_execution_mode(mode_str);
|
||||
info!(target = "udmin", "flow.run: ctx prepared from design_json id={} execution_mode={:?}", id, exec_mode);
|
||||
(chain_from_json, ctx)
|
||||
} else {
|
||||
info!(target = "udmin", "flow.run: parsing YAML id={}", id);
|
||||
let dsl = match serde_yaml::from_str::<FlowDSL>(&doc.yaml) {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
error!(target = "udmin", error = ?e, "flow.run: parse YAML failed id={}", id);
|
||||
// 记录失败日志
|
||||
let (user_id, username) = operator.as_ref().map(|(u, n)| (Some(*u), Some(n.clone()))).unwrap_or((None, None));
|
||||
let _ = flow_run_log_service::create(db, CreateRunLogInput {
|
||||
flow_id: id.to_string(),
|
||||
flow_code: flow_code.clone(),
|
||||
input: Some(serde_json::to_string(&req.input).unwrap_or_default()),
|
||||
output: None,
|
||||
ok: false,
|
||||
logs: Some(format!("parse YAML failed: {}", e)),
|
||||
user_id,
|
||||
username,
|
||||
started_at: start,
|
||||
duration_ms: 0,
|
||||
}).await;
|
||||
return Err(anyhow::Error::new(e).context("invalid flow yaml"));
|
||||
}
|
||||
};
|
||||
// 从 YAML 读取执行模式
|
||||
if let Some(m) = dsl.execution_mode.as_deref() { exec_mode = parse_execution_mode(m); }
|
||||
(dsl.into(), req.input.clone())
|
||||
};
|
||||
|
||||
// 若 design_json 解析出的 chain 为空,兜底回退到 YAML
|
||||
if chain.nodes.is_empty() {
|
||||
info!(target = "udmin", "flow.run: empty chain from design_json, fallback to YAML id={}", id);
|
||||
if !doc.yaml.trim().is_empty() {
|
||||
match serde_yaml::from_str::<FlowDSL>(&doc.yaml) {
|
||||
Ok(dsl) => {
|
||||
chain = dsl.clone().into();
|
||||
// YAML 分支下 ctx = req.input,不再追加 design_json 的补充
|
||||
ctx = req.input.clone();
|
||||
if let Some(m) = dsl.execution_mode.as_deref() { exec_mode = parse_execution_mode(m); }
|
||||
info!(target = "udmin", "flow.run: fallback YAML parsed id={} execution_mode={:?}", id, exec_mode);
|
||||
}
|
||||
Err(e) => {
|
||||
error!(target = "udmin", error = ?e, "flow.run: fallback parse YAML failed id={}", id);
|
||||
// 保留原空 chain,稍后 drive 会再次报错,但这里先返回更明确的错误
|
||||
let (user_id, username) = operator.as_ref().map(|(u, n)| (Some(*u), Some(n.clone()))).unwrap_or((None, None));
|
||||
let _ = flow_run_log_service::create(db, CreateRunLogInput {
|
||||
flow_id: id.to_string(),
|
||||
flow_code: flow_code.clone(),
|
||||
input: Some(serde_json::to_string(&req.input).unwrap_or_default()),
|
||||
output: None,
|
||||
ok: false,
|
||||
logs: Some(format!("fallback parse YAML failed: {}", e)),
|
||||
user_id,
|
||||
username,
|
||||
started_at: start,
|
||||
duration_ms: 0,
|
||||
}).await;
|
||||
return Err(anyhow::anyhow!("empty chain: design_json produced no nodes and YAML parse failed"));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// YAML 也为空
|
||||
let (user_id, username) = operator.as_ref().map(|(u, n)| (Some(*u), Some(n.clone()))).unwrap_or((None, None));
|
||||
let _ = flow_run_log_service::create(db, CreateRunLogInput {
|
||||
flow_id: id.to_string(),
|
||||
flow_code: flow_code.clone(),
|
||||
input: Some(serde_json::to_string(&req.input).unwrap_or_default()),
|
||||
output: None,
|
||||
ok: false,
|
||||
logs: Some("empty chain: both design_json and yaml are empty".to_string()),
|
||||
user_id,
|
||||
username,
|
||||
started_at: start,
|
||||
duration_ms: 0,
|
||||
}).await;
|
||||
return Err(anyhow::anyhow!("empty chain: both design_json and yaml are empty"));
|
||||
}
|
||||
}
|
||||
|
||||
// 从全局注册中心获取任务(若未初始化则返回默认注册表)
|
||||
let tasks: flow::task::TaskRegistry = flow::task::get_registry();
|
||||
let engine = FlowEngine::new(tasks);
|
||||
|
||||
info!(target = "udmin", "flow.run: driving engine id={} nodes={} links={} execution_mode={:?}", id, chain.nodes.len(), chain.links.len(), exec_mode);
|
||||
// 执行
|
||||
let drive_res = engine
|
||||
.drive(&chain, ctx, DriveOptions { execution_mode: exec_mode.clone(), ..Default::default() })
|
||||
.await;
|
||||
let (ctx, logs) = match drive_res {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
error!(target = "udmin", error = ?e, "flow.run: engine drive failed id={}", id);
|
||||
let dur = (Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap()) - start).num_milliseconds() as i64;
|
||||
let (user_id, username) = operator.as_ref().map(|(u, n)| (Some(*u), Some(n.clone()))).unwrap_or((None, None));
|
||||
let _ = flow_run_log_service::create(db, CreateRunLogInput {
|
||||
flow_id: id.to_string(),
|
||||
flow_code: flow_code.clone(),
|
||||
input: Some(serde_json::to_string(&req.input).unwrap_or_default()),
|
||||
output: None,
|
||||
ok: false,
|
||||
logs: Some(format!("engine drive failed: {}", e)),
|
||||
user_id,
|
||||
username,
|
||||
started_at: start,
|
||||
duration_ms: dur,
|
||||
}).await;
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
// 调试:打印处理后的 ctx
|
||||
//info!(target = "udmin", "flow.run: result ctx={}", serde_json::to_string(&ctx).unwrap_or_else(|_| "<serialize ctx failed>".to_string()));
|
||||
|
||||
info!(target = "udmin", "flow.run: done id={}", id);
|
||||
// 写入成功日志
|
||||
let dur = (Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap()) - start).num_milliseconds() as i64;
|
||||
let (user_id, username) = operator.map(|(u, n)| (Some(u), Some(n))).unwrap_or((None, None));
|
||||
let _ = flow_run_log_service::create(db, CreateRunLogInput {
|
||||
flow_id: id.to_string(),
|
||||
flow_code: flow_code.clone(),
|
||||
input: Some(serde_json::to_string(&req.input).unwrap_or_default()),
|
||||
output: Some(serde_json::to_string(&ctx).unwrap_or_default()),
|
||||
ok: true,
|
||||
logs: Some(serde_json::to_string(&logs).unwrap_or_default()),
|
||||
user_id,
|
||||
username,
|
||||
started_at: start,
|
||||
duration_ms: dur,
|
||||
}).await;
|
||||
Ok(RunResult { ok: true, ctx, logs })
|
||||
}
|
||||
|
||||
fn extract_name(yaml: &str) -> Option<String> {
|
||||
for line in yaml.lines() {
|
||||
let lt = line.trim();
|
||||
if lt.starts_with("#") && lt.len() > 1 { return Some(lt.trim_start_matches('#').trim().to_string()); }
|
||||
if lt.starts_with("name:") {
|
||||
let name = lt.trim_start_matches("name:").trim();
|
||||
if !name.is_empty() { return Some(name.to_string()); }
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn ae<E: Into<anyhow::Error>>(e: E) -> AppError {
|
||||
let err: anyhow::Error = e.into();
|
||||
let mut full = err.to_string();
|
||||
for cause in err.chain().skip(1) {
|
||||
full.push_str(" | ");
|
||||
full.push_str(&cause.to_string());
|
||||
}
|
||||
// MySQL duplicate key example: "Database error: Duplicate entry 'xxx' for key 'idx-unique-flows-code'"
|
||||
// 也兼容包含唯一索引名/关键字的报错信息
|
||||
if full.contains("Duplicate entry") || full.contains("idx-unique-flows-code") || (full.contains("code") && full.contains("unique")) {
|
||||
return AppError::Conflict("流程编码已存在".to_string());
|
||||
}
|
||||
AppError::Anyhow(anyhow::anyhow!(full))
|
||||
}
|
||||
|
||||
// shallow merge json objects: a <- b
|
||||
fn merge_json(a: &mut serde_json::Value, b: &serde_json::Value) {
|
||||
use serde_json::Value as V;
|
||||
match (a, b) {
|
||||
(V::Object(ao), V::Object(bo)) => {
|
||||
for (k, v) in bo.iter() {
|
||||
match ao.get_mut(k) {
|
||||
Some(av) => merge_json(av, v),
|
||||
None => { ao.insert(k.clone(), v.clone()); }
|
||||
}
|
||||
}
|
||||
}
|
||||
(a_slot, b_val) => { *a_slot = b_val.clone(); }
|
||||
}
|
||||
}
|
||||
|
||||
// parse execution mode string
|
||||
fn parse_execution_mode(s: &str) -> ExecutionMode {
|
||||
match s.to_ascii_lowercase().as_str() {
|
||||
"async" | "async_fire_and_forget" | "fire_and_forget" => ExecutionMode::AsyncFireAndForget,
|
||||
_ => ExecutionMode::Sync,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user