improve code for notify

This commit is contained in:
houseme
2025-06-23 03:34:05 +08:00
parent c7af6587f5
commit 928453db62
30 changed files with 521 additions and 989 deletions
-3
View File
@@ -3,8 +3,5 @@ pub mod constants;
#[cfg(feature = "constants")]
pub use constants::app::*;
#[cfg(feature = "notify")]
pub mod notify;
#[cfg(feature = "observability")]
pub mod observability;
-53
View File
@@ -1,53 +0,0 @@
use crate::notify::mqtt::MQTTArgs;
use crate::notify::webhook::WebhookArgs;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// Config - notification target configuration structure, holds
/// information about various notification targets.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NotifyConfig {
pub mqtt: HashMap<String, MQTTArgs>,
pub webhook: HashMap<String, WebhookArgs>,
}
impl NotifyConfig {
/// Create a new configuration with default values.
pub fn new() -> Self {
let mut config = NotifyConfig {
webhook: HashMap::new(),
mqtt: HashMap::new(),
};
// Insert default target for each backend
config.webhook.insert("1".to_string(), WebhookArgs::new());
config.mqtt.insert("1".to_string(), MQTTArgs::new());
config
}
}
impl Default for NotifyConfig {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use crate::notify::config::NotifyConfig;
#[test]
fn test_notify_config_new() {
let config = NotifyConfig::new();
assert_eq!(config.webhook.len(), 1);
assert_eq!(config.mqtt.len(), 1);
assert!(config.webhook.contains_key("1"));
assert!(config.mqtt.contains_key("1"));
}
#[test]
fn test_notify_config_default() {
let config = NotifyConfig::default();
assert_eq!(config.webhook.len(), 1);
assert_eq!(config.mqtt.len(), 1);
}
}
-26
View File
@@ -1,26 +0,0 @@
/// Help text for Webhook configuration.
pub const HELP_WEBHOOK: &str = r#"
Webhook configuration:
- enable: Enable or disable the webhook target (true/false)
- endpoint: Webhook server endpoint (e.g., http://localhost:8080/rustfs/events)
- auth_token: Opaque string or JWT authorization token (optional)
- queue_dir: Absolute path for persistent event queue (optional)
- queue_limit: Maximum number of events to queue (optional, default: 0)
- client_cert: Path to client certificate file (optional)
- client_key: Path to client private key file (optional)
"#;
/// Help text for MQTT configuration.
pub const HELP_MQTT: &str = r#"
MQTT configuration:
- enable: Enable or disable the MQTT target (true/false)
- broker: MQTT broker address (e.g., tcp://localhost:1883)
- topic: MQTT topic (e.g., rustfs/events)
- qos: Quality of Service level (0, 1, or 2)
- username: Username for MQTT authentication (optional)
- password: Password for MQTT authentication (optional)
- reconnect_interval: Reconnect interval in milliseconds (optional)
- keep_alive_interval: Keep alive interval in milliseconds (optional)
- queue_dir: Absolute path for persistent event queue (optional)
- queue_limit: Maximum number of events to queue (optional, default: 0)
"#;
-268
View File
@@ -1,268 +0,0 @@
use crate::notify::mqtt::MQTTArgs;
use crate::notify::webhook::WebhookArgs;
use std::collections::HashMap;
/// Convert legacy webhook configuration to the new WebhookArgs struct.
pub fn convert_webhook_config(config: &HashMap<String, String>) -> Result<WebhookArgs, String> {
let mut args = WebhookArgs::new();
args.enable = config.get("enable").map_or(false, |v| v == "true");
args.endpoint = config.get("endpoint").unwrap_or(&"".to_string()).clone();
args.auth_token = config.get("auth_token").unwrap_or(&"".to_string()).clone();
args.queue_dir = config.get("queue_dir").unwrap_or(&"".to_string()).clone();
args.queue_limit = config.get("queue_limit").map_or(0, |v| v.parse().unwrap_or(0));
args.client_cert = config.get("client_cert").unwrap_or(&"".to_string()).clone();
args.client_key = config.get("client_key").unwrap_or(&"".to_string()).clone();
Ok(args)
}
/// Convert legacy MQTT configuration to the new MQTTArgs struct.
pub fn convert_mqtt_config(config: &HashMap<String, String>) -> Result<MQTTArgs, String> {
let mut args = MQTTArgs::new();
args.enable = config.get("enable").map_or(false, |v| v == "true");
args.broker = config.get("broker").unwrap_or(&"".to_string()).clone();
args.topic = config.get("topic").unwrap_or(&"".to_string()).clone();
args.qos = config.get("qos").map_or(0, |v| v.parse().unwrap_or(0));
args.username = config.get("username").unwrap_or(&"".to_string()).clone();
args.password = config.get("password").unwrap_or(&"".to_string()).clone();
args.reconnect_interval = config.get("reconnect_interval").map_or(0, |v| v.parse().unwrap_or(0));
args.keep_alive_interval = config.get("keep_alive_interval").map_or(0, |v| v.parse().unwrap_or(0));
args.queue_dir = config.get("queue_dir").unwrap_or(&"".to_string()).clone();
args.queue_limit = config.get("queue_limit").map_or(0, |v| v.parse().unwrap_or(0));
Ok(args)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_convert_webhook_config_invalid() {
let mut old_config = HashMap::new();
old_config.insert("max_retries".to_string(), "invalid".to_string());
let result = convert_webhook_config(&old_config);
assert!(result.is_err());
}
#[test]
fn test_convert_mqtt_config_invalid() {
let mut old_config = HashMap::new();
old_config.insert("port".to_string(), "invalid".to_string());
let result = convert_mqtt_config(&old_config);
assert!(result.is_err());
}
#[test]
fn test_convert_empty_config() {
let empty_config = HashMap::new();
let webhook_result = convert_webhook_config(&empty_config);
assert!(webhook_result.is_ok());
let mqtt_result = convert_mqtt_config(&empty_config);
assert!(mqtt_result.is_ok());
}
#[test]
fn test_convert_config_with_extra_fields() {
let mut extra_config = HashMap::new();
extra_config.insert("endpoint".to_string(), "http://example.com".to_string());
extra_config.insert("extra_field".to_string(), "extra_value".to_string());
let webhook_result = convert_webhook_config(&extra_config);
assert!(webhook_result.is_ok());
let args = webhook_result.unwrap();
assert_eq!(args.endpoint, "http://example.com");
let mut extra_mqtt_config = HashMap::new();
extra_mqtt_config.insert("broker".to_string(), "mqtt.example.com".to_string());
extra_mqtt_config.insert("extra_field".to_string(), "extra_value".to_string());
let mqtt_result = convert_mqtt_config(&extra_mqtt_config);
assert!(mqtt_result.is_ok());
let args = mqtt_result.unwrap();
assert_eq!(args.broker, "mqtt.example.com");
}
#[test]
fn test_convert_config_with_empty_values() {
let mut empty_values_config = HashMap::new();
empty_values_config.insert("endpoint".to_string(), "".to_string());
let webhook_result = convert_webhook_config(&empty_values_config);
assert!(webhook_result.is_ok());
let args = webhook_result.unwrap();
assert_eq!(args.endpoint, "");
let mut empty_mqtt_config = HashMap::new();
empty_mqtt_config.insert("broker".to_string(), "".to_string());
let mqtt_result = convert_mqtt_config(&empty_mqtt_config);
assert!(mqtt_result.is_ok());
let args = mqtt_result.unwrap();
assert_eq!(args.broker, "");
}
#[test]
fn test_convert_config_with_whitespace_values() {
let mut whitespace_config = HashMap::new();
whitespace_config.insert("endpoint".to_string(), " http://example.com ".to_string());
let webhook_result = convert_webhook_config(&whitespace_config);
assert!(webhook_result.is_ok());
let args = webhook_result.unwrap();
assert_eq!(args.endpoint, " http://example.com ");
let mut whitespace_mqtt_config = HashMap::new();
whitespace_mqtt_config.insert("broker".to_string(), " mqtt.example.com ".to_string());
let mqtt_result = convert_mqtt_config(&whitespace_mqtt_config);
assert!(mqtt_result.is_ok());
let args = mqtt_result.unwrap();
assert_eq!(args.broker, " mqtt.example.com ");
}
#[test]
fn test_convert_config_with_special_characters() {
let mut special_chars_config = HashMap::new();
special_chars_config.insert("endpoint".to_string(), "http://example.com/path?param=value&other=123".to_string());
let webhook_result = convert_webhook_config(&special_chars_config);
assert!(webhook_result.is_ok());
let args = webhook_result.unwrap();
assert_eq!(args.endpoint, "http://example.com/path?param=value&other=123");
let mut special_chars_mqtt_config = HashMap::new();
special_chars_mqtt_config.insert("broker".to_string(), "mqtt.example.com:1883".to_string());
let mqtt_result = convert_mqtt_config(&special_chars_mqtt_config);
assert!(mqtt_result.is_ok());
let args = mqtt_result.unwrap();
assert_eq!(args.broker, "mqtt.example.com:1883");
}
#[test]
fn test_convert_config_with_boolean_values() {
let mut boolean_config = HashMap::new();
boolean_config.insert("enable".to_string(), "true".to_string());
let webhook_result = convert_webhook_config(&boolean_config);
assert!(webhook_result.is_ok());
let args = webhook_result.unwrap();
assert_eq!(args.endpoint, ""); // default value
let mut boolean_mqtt_config = HashMap::new();
boolean_mqtt_config.insert("enable".to_string(), "false".to_string());
let mqtt_result = convert_mqtt_config(&boolean_mqtt_config);
assert!(mqtt_result.is_ok());
let args = mqtt_result.unwrap();
assert_eq!(args.broker, "localhost"); // default value
}
#[test]
fn test_convert_config_with_null_values() {
let mut null_config = HashMap::new();
null_config.insert("endpoint".to_string(), "null".to_string());
let webhook_result = convert_webhook_config(&null_config);
assert!(webhook_result.is_ok());
let args = webhook_result.unwrap();
assert_eq!(args.endpoint, "null");
let mut null_mqtt_config = HashMap::new();
null_mqtt_config.insert("broker".to_string(), "null".to_string());
let mqtt_result = convert_mqtt_config(&null_mqtt_config);
assert!(mqtt_result.is_ok());
let args = mqtt_result.unwrap();
assert_eq!(args.broker, "null");
}
#[test]
fn test_convert_config_with_duplicate_keys() {
let mut duplicate_config = HashMap::new();
duplicate_config.insert("endpoint".to_string(), "http://example.org".to_string());
let webhook_result = convert_webhook_config(&duplicate_config);
assert!(webhook_result.is_ok());
let args = webhook_result.unwrap();
assert_eq!(args.endpoint, "http://example.org"); // last value wins
let mut duplicate_mqtt_config = HashMap::new();
duplicate_mqtt_config.insert("broker".to_string(), "mqtt.example.org".to_string());
let mqtt_result = convert_mqtt_config(&duplicate_mqtt_config);
assert!(mqtt_result.is_ok());
let args = mqtt_result.unwrap();
assert_eq!(args.broker, "mqtt.example.org"); // last value wins
}
#[test]
fn test_convert_config_with_case_insensitive_keys() {
let mut case_insensitive_config = HashMap::new();
case_insensitive_config.insert("ENDPOINT".to_string(), "http://example.com".to_string());
let webhook_result = convert_webhook_config(&case_insensitive_config);
assert!(webhook_result.is_ok());
let args = webhook_result.unwrap();
assert_eq!(args.endpoint, "http://example.com");
let mut case_insensitive_mqtt_config = HashMap::new();
case_insensitive_mqtt_config.insert("BROKER".to_string(), "mqtt.example.com".to_string());
let mqtt_result = convert_mqtt_config(&case_insensitive_mqtt_config);
assert!(mqtt_result.is_ok());
let args = mqtt_result.unwrap();
assert_eq!(args.broker, "mqtt.example.com");
}
#[test]
fn test_convert_config_with_mixed_case_keys() {
let mut mixed_case_config = HashMap::new();
mixed_case_config.insert("EndPoint".to_string(), "http://example.com".to_string());
let webhook_result = convert_webhook_config(&mixed_case_config);
assert!(webhook_result.is_ok());
let args = webhook_result.unwrap();
assert_eq!(args.endpoint, "http://example.com");
let mut mixed_case_mqtt_config = HashMap::new();
mixed_case_mqtt_config.insert("BroKer".to_string(), "mqtt.example.com".to_string());
let mqtt_result = convert_mqtt_config(&mixed_case_mqtt_config);
assert!(mqtt_result.is_ok());
let args = mqtt_result.unwrap();
assert_eq!(args.broker, "mqtt.example.com");
}
#[test]
fn test_convert_config_with_snake_case_keys() {
let mut snake_case_config = HashMap::new();
snake_case_config.insert("end_point".to_string(), "http://example.com".to_string());
let webhook_result = convert_webhook_config(&snake_case_config);
assert!(webhook_result.is_ok());
let args = webhook_result.unwrap();
assert_eq!(args.endpoint, "http://example.com");
let mut snake_case_mqtt_config = HashMap::new();
snake_case_mqtt_config.insert("bro_ker".to_string(), "mqtt.example.com".to_string());
let mqtt_result = convert_mqtt_config(&snake_case_mqtt_config);
assert!(mqtt_result.is_ok());
let args = mqtt_result.unwrap();
assert_eq!(args.broker, "mqtt.example.com");
}
#[test]
fn test_convert_config_with_kebab_case_keys() {
let mut kebab_case_config = HashMap::new();
kebab_case_config.insert("end-point".to_string(), "http://example.com".to_string());
let webhook_result = convert_webhook_config(&kebab_case_config);
assert!(webhook_result.is_ok());
let args = webhook_result.unwrap();
assert_eq!(args.endpoint, "http://example.com");
let mut kebab_case_mqtt_config = HashMap::new();
kebab_case_mqtt_config.insert("bro-ker".to_string(), "mqtt.example.com".to_string());
let mqtt_result = convert_mqtt_config(&kebab_case_mqtt_config);
assert!(mqtt_result.is_ok());
let args = mqtt_result.unwrap();
assert_eq!(args.broker, "mqtt.example.com");
}
#[test]
fn test_convert_config_with_camel_case_keys() {
let mut camel_case_config = HashMap::new();
camel_case_config.insert("endPoint".to_string(), "http://example.com".to_string());
let webhook_result = convert_webhook_config(&camel_case_config);
assert!(webhook_result.is_ok());
let args = webhook_result.unwrap();
assert_eq!(args.endpoint, "http://example.com");
let mut camel_case_mqtt_config = HashMap::new();
camel_case_mqtt_config.insert("broKer".to_string(), "mqtt.example.com".to_string());
let mqtt_result = convert_mqtt_config(&camel_case_mqtt_config);
assert!(mqtt_result.is_ok());
let args = mqtt_result.unwrap();
assert_eq!(args.broker, "mqtt.example.com");
}
}
-5
View File
@@ -1,5 +0,0 @@
pub mod config;
pub mod help;
pub mod legacy;
pub mod mqtt;
pub mod webhook;
-114
View File
@@ -1,114 +0,0 @@
use serde::{Deserialize, Serialize};
/// MQTTArgs - MQTT target arguments.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MQTTArgs {
pub enable: bool,
pub broker: String,
pub topic: String,
pub qos: u8,
pub username: String,
pub password: String,
pub reconnect_interval: u64,
pub keep_alive_interval: u64,
#[serde(skip)]
pub root_cas: Option<()>, // Placeholder for *x509.CertPool
pub queue_dir: String,
pub queue_limit: u64,
}
impl MQTTArgs {
/// Create a new configuration with default values.
pub fn new() -> Self {
Self {
enable: false,
broker: "".to_string(),
topic: "".to_string(),
qos: 0,
username: "".to_string(),
password: "".to_string(),
reconnect_interval: 0,
keep_alive_interval: 0,
root_cas: None,
queue_dir: "".to_string(),
queue_limit: 0,
}
}
/// Validate MQTTArgs fields
pub fn validate(&self) -> Result<(), String> {
if !self.enable {
return Ok(());
}
if self.broker.trim().is_empty() {
return Err("MQTT broker cannot be empty".to_string());
}
if self.topic.trim().is_empty() {
return Err("MQTT topic cannot be empty".to_string());
}
if self.queue_dir != "" && !self.queue_dir.starts_with('/') {
return Err("queueDir path should be absolute".to_string());
}
if self.qos == 0 && self.queue_dir != "" {
return Err("qos should be set to 1 or 2 if queueDir is set".to_string());
}
Ok(())
}
}
impl Default for MQTTArgs {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mqtt_args_new() {
let args = MQTTArgs::new();
assert_eq!(args.broker, "");
assert_eq!(args.topic, "");
assert_eq!(args.qos, 0);
assert_eq!(args.username, "");
assert_eq!(args.password, "");
assert_eq!(args.reconnect_interval, 0);
assert_eq!(args.keep_alive_interval, 0);
assert!(args.root_cas.is_none());
assert_eq!(args.queue_dir, "");
assert_eq!(args.queue_limit, 0);
assert!(!args.enable);
}
#[test]
fn test_mqtt_args_validate() {
let mut args = MQTTArgs::new();
assert!(args.validate().is_ok());
args.broker = "".to_string();
assert!(args.validate().is_err());
args.broker = "localhost".to_string();
args.topic = "".to_string();
assert!(args.validate().is_err());
args.topic = "mqtt_topic".to_string();
args.reconnect_interval = 10001;
assert!(args.validate().is_err());
args.reconnect_interval = 1000;
args.keep_alive_interval = 10001;
assert!(args.validate().is_err());
args.keep_alive_interval = 1000;
args.queue_limit = 10001;
assert!(args.validate().is_err());
args.queue_dir = "invalid_path".to_string();
assert!(args.validate().is_err());
args.queue_dir = "/valid_path".to_string();
assert!(args.validate().is_ok());
args.qos = 0;
assert!(args.validate().is_err());
args.qos = 1;
assert!(args.validate().is_ok());
args.qos = 2;
assert!(args.validate().is_ok());
}
}
-81
View File
@@ -1,81 +0,0 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// WebhookArgs - Webhook target arguments.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebhookArgs {
pub enable: bool,
pub endpoint: String,
pub auth_token: String,
#[serde(skip)]
pub custom_headers: Option<HashMap<String, String>>,
pub queue_dir: String,
pub queue_limit: u64,
pub client_cert: String,
pub client_key: String,
}
impl WebhookArgs {
/// Create a new configuration with default values.
pub fn new() -> Self {
Self {
enable: false,
endpoint: "".to_string(),
auth_token: "".to_string(),
custom_headers: None,
queue_dir: "".to_string(),
queue_limit: 0,
client_cert: "".to_string(),
client_key: "".to_string(),
}
}
/// Validate WebhookArgs fields
pub fn validate(&self) -> Result<(), String> {
if !self.enable {
return Ok(());
}
if self.endpoint.trim().is_empty() {
return Err("endpoint empty".to_string());
}
if self.queue_dir != "" && !self.queue_dir.starts_with('/') {
return Err("queueDir path should be absolute".to_string());
}
if (self.client_cert != "" && self.client_key == "") || (self.client_cert == "" && self.client_key != "") {
return Err("cert and key must be specified as a pair".to_string());
}
Ok(())
}
}
impl Default for WebhookArgs {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use crate::notify::webhook::WebhookArgs;
#[test]
fn test_webhook_args_new() {
let args = WebhookArgs::new();
assert_eq!(args.endpoint, "");
assert_eq!(args.auth_token, "");
assert!(args.custom_headers.is_none());
assert_eq!(args.queue_dir, "");
assert_eq!(args.queue_limit, 0);
assert_eq!(args.client_cert, "");
assert_eq!(args.client_key, "");
assert!(!args.enable);
}
#[test]
fn test_webhook_args_validate() {
let mut args = WebhookArgs::new();
assert!(args.validate().is_err());
args.endpoint = "http://example.com".to_string();
assert!(args.validate().is_ok());
}
}