use serde::{Deserialize, Serialize};
#[derive(Debug, Clone)]
pub struct ValidationResult {
pub valid: bool,
pub issues: Vec<ConfigIssue>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigIssue {
pub field: String,
pub message: String,
pub suggestion: String,
pub severity: String,
}
impl ValidationResult {
pub fn has_errors(&self) -> bool {
self.issues.iter().any(|i| i.severity == "error")
}
pub fn errors(&self) -> impl Iterator<Item = &ConfigIssue> {
self.issues.iter().filter(|i| i.severity == "error")
}
}
pub fn validate_coordinator_config(
addr: &str,
max_sessions: usize,
session_timeout_secs: u64,
) -> ValidationResult {
let mut issues = Vec::new();
if addr.is_empty() {
issues.push(ConfigIssue {
field: "addr".into(),
message: "address is empty".into(),
suggestion: "set to e.g. '0.0.0.0:18432'".into(),
severity: "error".into(),
});
} else if !addr.contains(':') {
issues.push(ConfigIssue {
field: "addr".into(),
message: "address missing port".into(),
suggestion: "format: 'host:port'".into(),
severity: "error".into(),
});
}
if max_sessions == 0 {
issues.push(ConfigIssue {
field: "max_sessions".into(),
message: "max_sessions is 0 (unlimited)".into(),
suggestion: "set a positive limit for production".into(),
severity: "warning".into(),
});
}
if session_timeout_secs < 60 {
issues.push(ConfigIssue {
field: "session_timeout_secs".into(),
message: "timeout < 60 seconds may be too short".into(),
suggestion: "use at least 300 (5 min) for production".into(),
severity: "warning".into(),
});
}
ValidationResult {
valid: !issues.iter().any(|i| i.severity == "error"),
issues,
}
}
pub fn validate_signer_config(
coordinator_addr: &str,
signer_id: &str,
quorum_id: &str,
share_file: &str,
) -> ValidationResult {
let mut issues = Vec::new();
if signer_id.is_empty() {
issues.push(ConfigIssue {
field: "signer_id".into(),
message: "signer_id is empty".into(),
suggestion: "set a unique signer identity".into(),
severity: "error".into(),
});
}
if quorum_id.is_empty() {
issues.push(ConfigIssue {
field: "quorum_id".into(),
message: "quorum_id is empty".into(),
suggestion: "set the quorum this signer belongs to".into(),
severity: "error".into(),
});
}
if coordinator_addr.is_empty() || !coordinator_addr.contains(':') {
issues.push(ConfigIssue {
field: "coordinator_addr".into(),
message: "coordinator_addr is invalid".into(),
suggestion: "format: 'host:port'".into(),
severity: "error".into(),
});
}
if share_file.is_empty() {
issues.push(ConfigIssue {
field: "share_file".into(),
message: "share_file is empty".into(),
suggestion: "set path to the share JSON file".into(),
severity: "error".into(),
});
}
ValidationResult {
valid: !issues.iter().any(|i| i.severity == "error"),
issues,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_coordinator_config_passes() {
let result = validate_coordinator_config("0.0.0.0:18432", 100, 3600);
assert!(result.valid);
assert!(result.issues.is_empty());
}
#[test]
fn empty_addr_rejected() {
let result = validate_coordinator_config("", 100, 3600);
assert!(!result.valid);
assert!(result.has_errors());
}
#[test]
fn missing_port_rejected() {
let result = validate_coordinator_config("localhost", 100, 3600);
assert!(!result.valid);
}
#[test]
fn unlimited_sessions_warns() {
let result = validate_coordinator_config("0.0.0.0:80", 0, 3600);
assert!(result.valid); assert!(!result.has_errors());
}
#[test]
fn short_timeout_warns() {
let result = validate_coordinator_config("0.0.0.0:80", 10, 30);
assert!(result.valid);
assert!(
result
.issues
.iter()
.any(|i| i.field == "session_timeout_secs")
);
}
#[test]
fn valid_signer_config_passes() {
let result =
validate_signer_config("localhost:18432", "alice", "quorum-1", "/shares/a.json");
assert!(result.valid);
}
#[test]
fn empty_signer_id_rejected() {
let result = validate_signer_config("localhost:18432", "", "q", "/s.json");
assert!(!result.valid);
}
#[test]
fn empty_share_file_rejected() {
let result = validate_signer_config("localhost:18432", "alice", "q", "");
assert!(!result.valid);
}
#[test]
fn issue_has_suggestion() {
let result = validate_coordinator_config("", 10, 3600);
for issue in &result.issues {
assert!(!issue.suggestion.is_empty());
}
}
#[test]
fn errors_filtered_correctly() {
let result = validate_coordinator_config("", 0, 30);
let errors: Vec<_> = result.errors().collect();
assert!(errors.iter().any(|e| e.severity == "error"));
assert!(result.issues.iter().any(|i| i.severity == "warning"));
}
}