use super::command::{CommandType, OutputFormat};
use anyhow::Result;
use async_trait::async_trait;
use serde_json::Value as JsonValue;
use std::collections::HashMap;
use std::time::Duration;
pub struct OutputProcessor {
pub formatters: HashMap<CommandType, Box<dyn OutputFormatter>>,
pub parsers: HashMap<OutputFormat, Box<dyn OutputParser>>,
}
impl OutputProcessor {
pub fn new() -> Self {
let mut formatters: HashMap<CommandType, Box<dyn OutputFormatter>> = HashMap::new();
formatters.insert(CommandType::Claude, Box::new(ClaudeOutputFormatter));
formatters.insert(CommandType::Shell, Box::new(ShellOutputFormatter));
formatters.insert(CommandType::Test, Box::new(TestOutputFormatter));
formatters.insert(CommandType::Handler, Box::new(HandlerOutputFormatter));
let mut parsers: HashMap<OutputFormat, Box<dyn OutputParser>> = HashMap::new();
parsers.insert(OutputFormat::Json, Box::new(JsonOutputParser));
parsers.insert(OutputFormat::Yaml, Box::new(YamlOutputParser));
parsers.insert(OutputFormat::PlainText, Box::new(PlainTextOutputParser));
Self {
formatters,
parsers,
}
}
pub async fn process_output(
&self,
raw_output: ProcessOutput,
command_type: CommandType,
output_format: Option<OutputFormat>,
) -> Result<ProcessedOutput> {
let formatted_output = if let Some(formatter) = self.formatters.get(&command_type) {
formatter.format(&raw_output).await?
} else {
raw_output.clone()
};
let parsed_output = if let Some(ref format) = output_format {
if let Some(parser) = self.parsers.get(format) {
parser.parse(&formatted_output).await?
} else {
formatted_output
}
} else {
formatted_output
};
Ok(ProcessedOutput::new(parsed_output, output_format))
}
}
impl Default for OutputProcessor {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct ProcessOutput {
pub stdout: Option<String>,
pub stderr: Option<String>,
pub structured_data: Option<JsonValue>,
pub error_summary: Option<String>,
pub metadata: OutputMetadata,
}
impl Default for ProcessOutput {
fn default() -> Self {
Self::new()
}
}
impl ProcessOutput {
pub fn empty() -> Self {
Self {
stdout: None,
stderr: None,
structured_data: None,
error_summary: None,
metadata: OutputMetadata::default(),
}
}
pub fn new() -> Self {
Self::empty()
}
pub fn with_stdout(mut self, stdout: String) -> Self {
self.stdout = Some(stdout);
self
}
pub fn with_stderr(mut self, stderr: String) -> Self {
self.stderr = Some(stderr);
self
}
pub fn with_structured_data(mut self, data: Option<JsonValue>) -> Self {
self.structured_data = data;
self
}
}
#[derive(Debug, Clone, Default)]
pub struct OutputMetadata {
pub bytes_stdout: usize,
pub bytes_stderr: usize,
pub lines_stdout: usize,
pub lines_stderr: usize,
pub truncated: bool,
}
#[derive(Debug, Clone)]
pub struct ProcessedOutput {
pub content: ProcessOutput,
pub format: OutputFormat,
pub processing_duration: Duration,
pub warnings: Vec<String>,
}
impl ProcessedOutput {
pub fn new(content: ProcessOutput, format: Option<OutputFormat>) -> Self {
Self {
content,
format: format.unwrap_or(OutputFormat::PlainText),
processing_duration: Duration::from_secs(0),
warnings: Vec::new(),
}
}
}
#[async_trait]
pub trait OutputFormatter: Send + Sync {
async fn format(&self, output: &ProcessOutput) -> Result<ProcessOutput>;
}
#[async_trait]
pub trait OutputParser: Send + Sync {
async fn parse(&self, output: &ProcessOutput) -> Result<ProcessOutput>;
}
pub struct ClaudeOutputFormatter;
#[async_trait]
impl OutputFormatter for ClaudeOutputFormatter {
async fn format(&self, output: &ProcessOutput) -> Result<ProcessOutput> {
let mut formatted = output.clone();
if let Some(ref stdout) = output.stdout {
let cleaned = self.remove_claude_artifacts(stdout);
let structured = self.extract_structured_data(&cleaned)?;
formatted.stdout = Some(cleaned);
formatted.structured_data = structured;
}
if let Some(ref stderr) = output.stderr {
formatted.error_summary = self.extract_claude_error(stderr);
}
Ok(formatted)
}
}
impl ClaudeOutputFormatter {
pub fn remove_claude_artifacts(&self, stdout: &str) -> String {
stdout
.lines()
.filter(|line| {
!line.starts_with("Claude:") && !line.starts_with("[Claude]")
})
.collect::<Vec<_>>()
.join("\n")
}
pub fn extract_structured_data(&self, output: &str) -> Result<Option<JsonValue>> {
if let Some(start) = output.find("```json") {
if let Some(end) = output[start..].find("```\n").map(|i| start + i) {
let json_str = &output[start + 7..end].trim();
if let Ok(json) = serde_json::from_str(json_str) {
return Ok(Some(json));
}
}
}
if let Ok(json) = serde_json::from_str(output) {
return Ok(Some(json));
}
Ok(None)
}
pub fn extract_claude_error(&self, stderr: &str) -> Option<String> {
for line in stderr.lines() {
if line.contains("Error:") || line.contains("Failed:") {
return Some(line.to_string());
}
}
None
}
}
pub struct ShellOutputFormatter;
#[async_trait]
impl OutputFormatter for ShellOutputFormatter {
async fn format(&self, output: &ProcessOutput) -> Result<ProcessOutput> {
let mut formatted = output.clone();
if let Some(ref stderr) = output.stderr {
formatted.error_summary = self.extract_error_summary(stderr);
}
Ok(formatted)
}
}
impl ShellOutputFormatter {
pub fn extract_error_summary(&self, stderr: &str) -> Option<String> {
let error_patterns = [
"command not found",
"No such file or directory",
"Permission denied",
"fatal:",
"error:",
"Error:",
];
for line in stderr.lines() {
for pattern in &error_patterns {
if line.contains(pattern) {
return Some(line.to_string());
}
}
}
stderr
.lines()
.find(|l| !l.trim().is_empty())
.map(String::from)
}
}
pub struct TestOutputFormatter;
#[async_trait]
impl OutputFormatter for TestOutputFormatter {
async fn format(&self, output: &ProcessOutput) -> Result<ProcessOutput> {
let mut formatted = output.clone();
if let Some(ref stdout) = output.stdout {
formatted.structured_data = self.extract_test_results(stdout);
}
Ok(formatted)
}
}
impl TestOutputFormatter {
pub fn extract_test_results(&self, stdout: &str) -> Option<JsonValue> {
let mut results = serde_json::Map::new();
for line in stdout.lines() {
if line.contains("tests passed") || line.contains("test result:") {
results.insert("summary".to_string(), JsonValue::String(line.to_string()));
}
if line.contains("PASSED") {
results.insert(
"status".to_string(),
JsonValue::String("passed".to_string()),
);
}
if line.contains("FAILED") {
results.insert(
"status".to_string(),
JsonValue::String("failed".to_string()),
);
}
}
if !results.is_empty() {
Some(JsonValue::Object(results))
} else {
None
}
}
}
pub struct HandlerOutputFormatter;
#[async_trait]
impl OutputFormatter for HandlerOutputFormatter {
async fn format(&self, output: &ProcessOutput) -> Result<ProcessOutput> {
Ok(output.clone())
}
}
pub struct JsonOutputParser;
#[async_trait]
impl OutputParser for JsonOutputParser {
async fn parse(&self, output: &ProcessOutput) -> Result<ProcessOutput> {
let mut parsed = output.clone();
if let Some(ref stdout) = output.stdout {
if let Ok(json) = serde_json::from_str::<JsonValue>(stdout) {
parsed.structured_data = Some(json);
}
}
Ok(parsed)
}
}
pub struct YamlOutputParser;
#[async_trait]
impl OutputParser for YamlOutputParser {
async fn parse(&self, output: &ProcessOutput) -> Result<ProcessOutput> {
let mut parsed = output.clone();
if let Some(ref stdout) = output.stdout {
if let Ok(yaml_value) = serde_yaml::from_str::<serde_yaml::Value>(stdout) {
if let Ok(json_str) = serde_json::to_string(&yaml_value) {
if let Ok(json) = serde_json::from_str(&json_str) {
parsed.structured_data = Some(json);
}
}
}
}
Ok(parsed)
}
}
pub struct PlainTextOutputParser;
#[async_trait]
impl OutputParser for PlainTextOutputParser {
async fn parse(&self, output: &ProcessOutput) -> Result<ProcessOutput> {
Ok(output.clone())
}
}