use std::fmt::{Display, Formatter};
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum DslFlagKind {
#[default]
Scalar,
CommaSeparatedList,
RepeatedList,
Bool,
KeyValuePairs,
SchemaField,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DslFlag {
pub flag: String,
pub config_key: String,
pub description: String,
pub kind: DslFlagKind,
pub required: bool,
}
pub fn engine_common_dsl_flags() -> Vec<DslFlag> {
vec![
DslFlag {
flag: "--title".to_string(),
config_key: "title".to_string(),
description: "Custom display title for this node instance. \
Overrides the default node kind title shown on the canvas."
.to_string(),
kind: DslFlagKind::Scalar,
required: false,
},
DslFlag {
flag: "--timeout".to_string(),
config_key: "timeout_secs".to_string(),
description: "Engine-level execution timeout for this node in seconds. \
Overrides the project-level pipeline_node_timeout_secs. \
Clamped to 5–3600s."
.to_string(),
kind: DslFlagKind::Scalar,
required: false,
},
]
}
pub fn engine_common_fields() -> Vec<NodeFieldDef> {
vec![NodeFieldDef {
name: "timeout_secs".to_string(),
label: "Timeout (s)".to_string(),
field_type: NodeFieldType::Text,
help: Some(
"Engine-level execution timeout in seconds. Overrides project default. Range: 5–3600."
.to_string(),
),
placeholder: Some("30".to_string()),
..Default::default()
}]
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum NodeFieldType {
Text,
Textarea,
CodeEditor,
Select,
Datalist,
MethodButtons,
CopyUrl,
Checkbox,
Section,
MultiCheckbox,
KeyValuePairs,
ClaimsPairs,
ParamsBuilder,
Number,
SecureRequestBindings,
MatchCases,
SourceBindings,
}
impl Default for NodeFieldType {
fn default() -> Self {
Self::Text
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NodeFieldDataSource {
CredentialsAll,
CredentialsPostgres,
CredentialsJwt,
TemplatesPages,
CredentialsBrowser,
CredentialsOpenAi,
CredentialsSecureRequest,
AiTools,
FunctionPipelines,
CredentialJwtRoles,
CredentialsHttpAuth,
CredentialsWebhookAuth,
CredentialsByKind(String),
}
impl serde::Serialize for NodeFieldDataSource {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
Self::CredentialsAll => serializer.serialize_str("credentials_all"),
Self::CredentialsPostgres => serializer.serialize_str("credentials_postgres"),
Self::CredentialsJwt => serializer.serialize_str("credentials_jwt"),
Self::TemplatesPages => serializer.serialize_str("templates_pages"),
Self::CredentialsBrowser => serializer.serialize_str("credentials_browser"),
Self::CredentialsOpenAi => serializer.serialize_str("credentials_open_ai"),
Self::CredentialsSecureRequest => {
serializer.serialize_str("credentials_secure_request")
}
Self::AiTools => serializer.serialize_str("ai_tools"),
Self::FunctionPipelines => serializer.serialize_str("function_pipelines"),
Self::CredentialJwtRoles => serializer.serialize_str("credential_jwt_roles"),
Self::CredentialsHttpAuth => serializer.serialize_str("credentials_http_auth"),
Self::CredentialsWebhookAuth => serializer.serialize_str("credentials_webhook_auth"),
Self::CredentialsByKind(kind) => {
serializer.serialize_str(&format!("credentials:{}", kind))
}
}
}
}
impl<'de> serde::Deserialize<'de> for NodeFieldDataSource {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
match s.as_str() {
"credentials_all" => Ok(Self::CredentialsAll),
"credentials_postgres" => Ok(Self::CredentialsPostgres),
"credentials_jwt" => Ok(Self::CredentialsJwt),
"templates_pages" => Ok(Self::TemplatesPages),
"credentials_browser" => Ok(Self::CredentialsBrowser),
"credentials_open_ai" => Ok(Self::CredentialsOpenAi),
"credentials_secure_request" => Ok(Self::CredentialsSecureRequest),
"ai_tools" => Ok(Self::AiTools),
"function_pipelines" => Ok(Self::FunctionPipelines),
"credential_jwt_roles" => Ok(Self::CredentialJwtRoles),
"credentials_http_auth" => Ok(Self::CredentialsHttpAuth),
"credentials_webhook_auth" => Ok(Self::CredentialsWebhookAuth),
other => {
if let Some(kind) = other.strip_prefix("credentials:") {
Ok(Self::CredentialsByKind(kind.to_string()))
} else {
Err(serde::de::Error::unknown_variant(
other,
&[
"credentials_all",
"credentials_postgres",
"credentials_jwt",
"templates_pages",
"credentials_browser",
"credentials_open_ai",
"credentials_secure_request",
"ai_tools",
"function_pipelines",
"credential_jwt_roles",
"credentials_http_auth",
"credentials_webhook_auth",
"credentials:<kind>",
],
))
}
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct SelectOptionDef {
pub value: String,
pub label: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct SidebarItem {
pub label: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub type_hint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct SidebarSection {
pub title: String,
pub items: Vec<SidebarItem>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct NodeFieldDef {
pub name: String,
pub label: String,
#[serde(rename = "type", default)]
pub field_type: NodeFieldType,
#[serde(skip_serializing_if = "Option::is_none")]
pub help: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub placeholder: Option<String>,
#[serde(default)]
pub readonly: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub rows: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub language: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub options: Vec<SelectOptionDef>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data_source: Option<NodeFieldDataSource>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_value: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub sidebar: Vec<SidebarSection>,
#[serde(skip_serializing_if = "Option::is_none")]
pub span: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum LayoutItem {
Field(String),
Row { row: Vec<LayoutItem> },
Col { col: Vec<LayoutItem> },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PipelineGraph {
#[serde(default = "default_pipeline_kind")]
pub kind: String,
#[serde(default = "default_pipeline_version")]
pub version: String,
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<PipelineGraphMetadata>,
#[serde(default)]
pub entry_nodes: Vec<String>,
pub nodes: Vec<PipelineNode>,
pub edges: Vec<PipelineEdge>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PipelineGraphMetadata {
#[serde(default)]
pub locked: bool,
#[serde(default)]
pub settings: PipelineGraphSettings,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PipelineGraphSettings {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub invocation_retention: Option<PipelineInvocationRetention>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PipelineInvocationRetention {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_invocations: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_age_secs: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PipelineNode {
pub id: String,
pub kind: String,
#[serde(default, alias = "inputs")]
pub input_pins: Vec<String>,
#[serde(default, alias = "outputs")]
pub output_pins: Vec<String>,
#[serde(default)]
pub config: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PipelineEdge {
#[serde(alias = "from")]
pub from_node: String,
pub from_pin: String,
#[serde(alias = "to")]
pub to_node: String,
pub to_pin: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub struct NodeScriptBridge {
pub name: String,
#[serde(default)]
pub enabled: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct NodeAiToolDefinition {
#[serde(default)]
pub registered: bool,
#[serde(default)]
pub tool_name: String,
#[serde(default)]
pub tool_description: String,
#[serde(default)]
pub tool_input_schema: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct NodeDefinition {
pub kind: String,
pub title: String,
pub description: String,
#[serde(default)]
pub config_schema: Value,
#[serde(default)]
pub input_schema: Value,
#[serde(default)]
pub output_schema: Value,
#[serde(default)]
pub input_pins: Vec<String>,
#[serde(default)]
pub output_pins: Vec<String>,
#[serde(default)]
pub script_available: bool,
#[serde(default)]
pub script_bridge: Option<NodeScriptBridge>,
#[serde(default)]
pub ai_tool: NodeAiToolDefinition,
#[serde(default)]
pub dsl_flags: Vec<DslFlag>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub fields: Vec<NodeFieldDef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub layout: Vec<LayoutItem>,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub ui_category: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub ui_category_label: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct NodeScriptUsageContract {
pub available: bool,
#[serde(default)]
pub bridge_name: String,
#[serde(default)]
pub enabled: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct NodeToolUsageContract {
pub registered: bool,
#[serde(default)]
pub tool_name: String,
#[serde(default)]
pub tool_description: String,
#[serde(default)]
pub tool_input_schema: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct NodeUsageMatrix {
pub pipeline_node: bool,
pub script_usage: NodeScriptUsageContract,
pub tool_usage: NodeToolUsageContract,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct NodeContractItem {
pub kind: String,
pub title: String,
pub description: String,
#[serde(default)]
pub config_schema: Value,
#[serde(default)]
pub input_schema: Value,
#[serde(default)]
pub output_schema: Value,
#[serde(default)]
pub input_pins: Vec<String>,
#[serde(default)]
pub output_pins: Vec<String>,
pub usage_matrix: NodeUsageMatrix,
#[serde(default)]
pub dsl_flags: Vec<DslFlag>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub fields: Vec<NodeFieldDef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub layout: Vec<LayoutItem>,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub ui_category: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub ui_category_label: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub icon_url: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub icon_hash: String,
#[serde(default = "default_node_source")]
pub source: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub tier: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub credential_requirements: Vec<NodeCredentialRequirement>,
}
fn default_node_source() -> String {
"native".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct NodeCredentialRequirement {
pub kind: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub title: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub description: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub config_key: String,
#[serde(default)]
pub required: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub placeholder_names: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct NodeContractDocument {
pub ok: bool,
pub schema_version: &'static str,
pub source: &'static str,
#[serde(default)]
pub items: Vec<NodeContractItem>,
}
impl From<NodeDefinition> for NodeContractItem {
fn from(value: NodeDefinition) -> Self {
let (bridge_name, bridge_enabled) = value
.script_bridge
.as_ref()
.map(|bridge| (bridge.name.clone(), bridge.enabled))
.unwrap_or_else(|| (String::new(), false));
let source = if value.kind.starts_with("n.c.") {
"composite".to_string()
} else if value.kind.starts_with("n.wasm.") {
"wasm".to_string()
} else {
default_node_source()
};
let ui_category = value.ui_category;
let ui_category_label = value.ui_category_label;
Self {
kind: value.kind,
title: value.title,
description: value.description,
config_schema: value.config_schema,
input_schema: value.input_schema,
output_schema: value.output_schema,
input_pins: value.input_pins,
output_pins: value.output_pins,
dsl_flags: value.dsl_flags,
fields: value.fields,
layout: value.layout,
usage_matrix: NodeUsageMatrix {
pipeline_node: true,
script_usage: NodeScriptUsageContract {
available: value.script_available,
bridge_name,
enabled: bridge_enabled,
},
tool_usage: NodeToolUsageContract {
registered: value.ai_tool.registered,
tool_name: value.ai_tool.tool_name,
tool_description: value.ai_tool.tool_description,
tool_input_schema: value.ai_tool.tool_input_schema,
},
},
ui_category,
ui_category_label,
icon_url: String::new(),
icon_hash: String::new(),
source,
tier: String::new(),
credential_requirements: Vec::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Signal {
pub kind: String,
pub message: String,
#[serde(default)]
pub node_id: String,
#[serde(default)]
pub node_kind: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub data: Option<serde_json::Value>,
#[serde(default)]
pub at: String,
}
pub type StepEvent = Signal;
#[derive(Debug, Clone)]
pub struct ExecutionBus {
tx: tokio::sync::broadcast::Sender<Signal>,
}
impl ExecutionBus {
pub fn new(capacity: usize) -> Self {
let (tx, _) = tokio::sync::broadcast::channel(capacity);
Self { tx }
}
pub fn emit(&self, signal: Signal) {
let _ = self.tx.send(signal);
}
pub fn subscribe(&self) -> tokio::sync::broadcast::Receiver<Signal> {
self.tx.subscribe()
}
}
#[derive(Debug, Clone, Default)]
pub struct ExecuteOptions {
pub bus: Option<std::sync::Arc<ExecutionBus>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PipelineContext {
pub owner: String,
pub project: String,
pub pipeline: String,
pub request_id: String,
#[serde(default)]
pub route: String,
pub input: Value,
#[serde(default)]
pub trigger: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub placeholder: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeTraceEntry {
pub node_id: String,
pub node_kind: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config: Option<Value>,
pub duration_ms: u64,
pub input: Value,
pub output: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PipelineOutput {
pub value: Value,
pub trace: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub node_trace: Vec<NodeTraceEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PipelineError {
pub code: &'static str,
pub message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub node_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub node_kind: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub node_trace: Vec<NodeTraceEntry>,
}
impl PipelineError {
pub fn new(code: &'static str, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
node_id: None,
node_kind: None,
node_trace: vec![],
}
}
pub fn node(node_id: &str, node_kind: &str, code: &'static str, msg: &str) -> Self {
Self {
code,
message: msg.to_string(),
node_id: Some(node_id.to_string()),
node_kind: Some(node_kind.to_string()),
node_trace: vec![],
}
}
}
impl Display for PipelineError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.code, self.message)
}
}
impl std::error::Error for PipelineError {}
fn default_pipeline_kind() -> String {
"zebflow.pipeline".to_string()
}
fn default_pipeline_version() -> String {
"0.1".to_string()
}