use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeDefinition {
#[serde(rename = "type")]
pub node_type: String,
#[serde(default)]
pub config: serde_yaml::Value,
#[serde(default)]
pub description: Option<String>,
#[serde(default = "default_enabled")]
pub enabled: bool,
#[serde(default)]
pub retry: Option<RetryConfig>,
#[serde(default)]
pub timeout_ms: Option<u64>,
#[serde(default)]
pub labels: HashMap<String, String>,
}
fn default_enabled() -> bool {
true
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RetryConfig {
#[serde(default = "default_max_retries")]
pub max_attempts: u32,
#[serde(default = "default_retry_delay_ms")]
pub initial_delay_ms: u64,
#[serde(default = "default_max_delay_ms")]
pub max_delay_ms: u64,
#[serde(default = "default_backoff_multiplier")]
pub backoff_multiplier: f64,
#[serde(default = "default_jitter")]
pub jitter: bool,
}
fn default_max_retries() -> u32 {
3
}
fn default_retry_delay_ms() -> u64 {
1000
}
fn default_max_delay_ms() -> u64 {
30_000
}
fn default_backoff_multiplier() -> f64 {
2.0
}
fn default_jitter() -> bool {
true
}
impl Default for RetryConfig {
fn default() -> Self {
Self {
max_attempts: default_max_retries(),
initial_delay_ms: default_retry_delay_ms(),
max_delay_ms: default_max_delay_ms(),
backoff_multiplier: default_backoff_multiplier(),
jitter: default_jitter(),
}
}
}
impl NodeDefinition {
pub fn new(node_type: impl Into<String>) -> Self {
Self {
node_type: node_type.into(),
config: serde_yaml::Value::Null,
description: None,
enabled: true,
retry: None,
timeout_ms: None,
labels: HashMap::new(),
}
}
pub fn with_config(mut self, config: serde_yaml::Value) -> Self {
self.config = config;
self
}
pub fn with_description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
pub fn with_retry(mut self, retry: RetryConfig) -> Self {
self.retry = Some(retry);
self
}
pub fn with_timeout_ms(mut self, ms: u64) -> Self {
self.timeout_ms = Some(ms);
self
}
pub fn with_label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.labels.insert(key.into(), value.into());
self
}
pub fn disabled(mut self) -> Self {
self.enabled = false;
self
}
pub fn is_std(&self) -> bool {
self.node_type.starts_with("std::")
}
pub fn is_trigger(&self) -> bool {
self.node_type.starts_with("trigger::")
}
pub fn get_string(&self, key: &str) -> Option<&str> {
self.config.get(key).and_then(|v| v.as_str())
}
pub fn get_i64(&self, key: &str) -> Option<i64> {
self.config.get(key).and_then(|v| v.as_i64())
}
pub fn get_bool(&self, key: &str) -> Option<bool> {
self.config.get(key).and_then(|v| v.as_bool())
}
pub fn get_nested(&self, path: &[&str]) -> Option<&serde_yaml::Value> {
let mut current = &self.config;
for key in path {
current = current.get(key)?;
}
Some(current)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deserialize_simple_node() {
let yaml = r#"
type: std::log
config:
message: "Hello"
level: info
"#;
let node: NodeDefinition = serde_yaml::from_str(yaml).unwrap();
assert_eq!(node.node_type, "std::log");
assert_eq!(node.get_string("message"), Some("Hello"));
assert!(node.enabled);
assert!(node.is_std());
}
#[test]
fn deserialize_node_with_retry() {
let yaml = r#"
type: plugins::http_call
config:
url: "https://api.example.com"
retry:
max_attempts: 5
initial_delay_ms: 500
"#;
let node: NodeDefinition = serde_yaml::from_str(yaml).unwrap();
assert!(node.retry.is_some());
let retry = node.retry.unwrap();
assert_eq!(retry.max_attempts, 5);
assert_eq!(retry.initial_delay_ms, 500);
}
#[test]
fn deserialize_node_with_labels() {
let yaml = r#"
type: std::switch
labels:
team: payments
tier: critical
"#;
let node: NodeDefinition = serde_yaml::from_str(yaml).unwrap();
assert_eq!(node.labels.get("team"), Some(&"payments".to_string()));
assert_eq!(node.labels.get("tier"), Some(&"critical".to_string()));
}
#[test]
fn node_builder() {
let node = NodeDefinition::new("std::merge")
.with_description("Wait for all inputs")
.with_timeout_ms(5000)
.with_label("category", "flow-control");
assert_eq!(node.node_type, "std::merge");
assert_eq!(node.description, Some("Wait for all inputs".to_string()));
assert_eq!(node.timeout_ms, Some(5000));
assert!(node.is_std());
}
#[test]
fn nested_config_access() {
let yaml = r#"
type: std::switch
config:
condition:
type: greater_than
field: amount
value: 100
"#;
let node: NodeDefinition = serde_yaml::from_str(yaml).unwrap();
let condition_type = node
.get_nested(&["condition", "type"])
.and_then(|v| v.as_str());
assert_eq!(condition_type, Some("greater_than"));
}
#[test]
fn disabled_node() {
let yaml = r#"
type: std::log
enabled: false
"#;
let node: NodeDefinition = serde_yaml::from_str(yaml).unwrap();
assert!(!node.enabled);
}
}