use crate::utils::error::{Error, Result};
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CriticalError {
pub code: String,
pub message: String,
pub context: String,
pub recovery_steps: Vec<String>,
pub documentation_link: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Warning {
pub code: String,
pub message: String,
pub suggestion: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "level", rename_all = "lowercase")]
pub enum AndonSignal {
Red(CriticalError),
Yellow(Warning),
Green,
}
impl AndonSignal {
pub fn enforce(&self) -> Result<()> {
match self {
AndonSignal::Red(error) => {
eprint_red_signal(error);
Err(Error::new(&format!(
"error[{}]: {}",
error.code, error.message
)))
}
AndonSignal::Yellow(warning) => {
eprint_yellow_signal(warning);
Ok(())
}
AndonSignal::Green => {
eprintln!("✓ 🟢 All Andon checks GREEN - proceeding");
Ok(())
}
}
}
pub fn manifest_invalid(missing_fields: Vec<String>) -> Self {
AndonSignal::Red(CriticalError {
code: "MANIFEST_INVALID".to_string(),
message: format!(
"Manifest validation failed - {} required field(s) missing",
missing_fields.len()
),
context: format!(
"File: ggen.toml\nMissing fields:\n{}",
missing_fields
.iter()
.enumerate()
.map(|(i, f)| format!(" {}. {}", i + 1, f))
.collect::<Vec<String>>()
.join("\n")
),
recovery_steps: vec![
"Open ggen.toml in your editor".to_string(),
"Add the missing required fields under appropriate sections".to_string(),
"Use 'ggen sync --validate-only' to check without generating".to_string(),
],
documentation_link: "https://ggen.dev/docs/manifest".to_string(),
})
}
pub fn manifest_error(path: &str, reason: &str) -> Self {
AndonSignal::Red(CriticalError {
code: "MANIFEST_ERROR".to_string(),
message: format!("Manifest error in {}", path),
context: reason.to_string(),
recovery_steps: vec![
format!("Check if {} exists and is readable", path),
"Run 'ggen init' if you need to create a new manifest".to_string(),
],
documentation_link: "https://ggen.dev/docs/manifest".to_string(),
})
}
pub fn circular_dependency(cycles: Vec<Vec<String>>) -> Self {
let cycle_display = cycles
.iter()
.map(|cycle| {
cycle
.iter()
.map(|f| f.to_string())
.collect::<Vec<_>>()
.join(" → ")
})
.collect::<Vec<_>>()
.join("\n ");
AndonSignal::Red(CriticalError {
code: "CIRCULAR_DEPENDENCY".to_string(),
message: "Circular dependency detected in ontology imports".to_string(),
context: format!(
"Cycle found:\n {}\n\nThis creates infinite loop in dependency resolution",
cycle_display
),
recovery_steps: vec![
"Review import statements in affected files".to_string(),
"Restructure as directed acyclic graph (DAG)".to_string(),
"Option A: Create root ontology that imports all".to_string(),
"Option B: Move shared definitions to base ontology".to_string(),
"Use `ggen sync --validate-only` to verify".to_string(),
],
documentation_link: "https://ggen.dev/docs/ontology-structure".to_string(),
})
}
pub fn permission_denied(path: &str, reason: &str) -> Self {
AndonSignal::Red(CriticalError {
code: "OUTPUT_DIR_NOT_WRITABLE".to_string(),
message: "Cannot write to output directory".to_string(),
context: format!(
"Path: {}\nReason: {}\n\nCheck directory permissions",
path, reason
),
recovery_steps: vec![
format!("Make directory writable: chmod u+w {}", path),
"Or change output_dir in ggen.toml [generation] section".to_string(),
"(Not recommended) Run with sudo".to_string(),
],
documentation_link: "https://ggen.dev/docs/permissions".to_string(),
})
}
pub fn unused_files(files: Vec<String>) -> Self {
AndonSignal::Yellow(Warning {
code: "UNUSED_FILES".to_string(),
message: format!("Found {} unused ontology files:", files.len()),
suggestion: if !files.is_empty() {
format!(
"If these aren't needed, delete them.\nOtherwise add to imports: ontology.imports = [{}]",
files.iter()
.map(|f| format!("\"{}\"", f))
.collect::<Vec<_>>()
.join(", ")
)
} else {
"No unused files".to_string()
},
})
}
pub fn performance_warning(duration_ms: u64, target_ms: u64) -> Self {
AndonSignal::Yellow(Warning {
code: "PERFORMANCE_SLOW".to_string(),
message: format!(
"Performance: Generation took {}ms (target: <{}ms)",
duration_ms, target_ms
),
suggestion: "Consider optimizing SPARQL queries or enabling caching.".to_string(),
})
}
pub fn is_red(&self) -> bool {
matches!(self, AndonSignal::Red(_))
}
pub fn is_yellow(&self) -> bool {
matches!(self, AndonSignal::Yellow(_))
}
pub fn is_green(&self) -> bool {
matches!(self, AndonSignal::Green)
}
}
impl fmt::Display for AndonSignal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AndonSignal::Red(error) => {
write!(f, "🔴 RED SIGNAL: {}", error.message)
}
AndonSignal::Yellow(warning) => {
write!(f, "🟡 YELLOW SIGNAL: {}", warning.message)
}
AndonSignal::Green => {
write!(f, "🟢 GREEN SIGNAL - All checks passed")
}
}
}
}
fn eprint_red_signal(error: &CriticalError) {
eprintln!();
eprintln!("╔════════════════════════════════════════════╗");
eprintln!("║ 🔴 ANDON SIGNAL: RED - STOP IMMEDIATELY ║");
eprintln!("╚════════════════════════════════════════════╝");
eprintln!();
eprintln!("Error Code: {}", error.code);
eprintln!("Message: {}", error.message);
eprintln!();
eprintln!("Context:");
for line in error.context.lines() {
eprintln!(" {}", line);
}
eprintln!();
eprintln!("Recovery Steps:");
for (i, step) in error.recovery_steps.iter().enumerate() {
eprintln!(" {}. {}", i + 1, step);
}
eprintln!();
eprintln!("📖 Learn more: {}", error.documentation_link);
eprintln!();
eprintln!("Sync STOPPED. Fix error above and retry.");
eprintln!();
}
fn eprint_yellow_signal(warning: &Warning) {
eprintln!();
eprintln!("⚠️ ANDON SIGNAL: YELLOW - CAUTION");
eprintln!("{}", warning.message);
eprintln!();
eprintln!(" Suggestion: {}", warning.suggestion);
eprintln!();
eprintln!("Continuing (use --strict to error on warnings)");
eprintln!();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_red_signal_manifest_invalid() {
let signal = AndonSignal::manifest_invalid(vec!["[ontology].source".to_string()]);
assert!(signal.is_red());
}
#[test]
fn test_yellow_signal() {
let signal = AndonSignal::Yellow(Warning {
code: "TEST".to_string(),
message: "Test warning".to_string(),
suggestion: "Do this".to_string(),
});
assert!(signal.is_yellow());
}
#[test]
fn test_green_signal() {
let signal = AndonSignal::Green;
assert!(signal.is_green());
}
}