use std::fs::{self, OpenOptions};
use std::io::{BufWriter, Write};
use std::path::Path;
use crate::error::{LoggerError, LoggerResult};
use crate::format::LogInfo;
use crate::core::config::LogLevel;
#[derive(Debug)]
pub struct JsonWriter;
impl JsonWriter {
pub fn new() -> Self {
Self
}
pub fn write_log_entry(&self, log_info: &LogInfo, file_path: &Path) -> LoggerResult<()> {
let json_string = self.format_as_json(log_info);
self.ensure_directory_exists(file_path)?;
let file = OpenOptions::new()
.create(true)
.append(true)
.open(file_path)
.map_err(|_| LoggerError::FileCreationFailed {
path: file_path.display().to_string(),
reason: "Failed to open JSON file for writing".to_string(),
})?;
let mut writer = BufWriter::new(file);
writeln!(writer, "{}", json_string)
.map_err(|_| LoggerError::DiskFull {
path: file_path.display().to_string(),
bytes_attempted: json_string.len() + 1,
})?;
writer.flush()
.map_err(|_| LoggerError::DiskFull {
path: file_path.display().to_string(),
bytes_attempted: json_string.len() + 1,
})?;
Ok(())
}
fn format_as_json(&self, log_info: &LogInfo) -> String {
let mut json_parts = Vec::new();
json_parts.push(format!("\"timestamp\":\"{}\"", self.escape_json_string(log_info.timestamp)));
json_parts.push(format!("\"level\":\"{}\"", log_info.level.as_str()));
json_parts.push(format!("\"message\":\"{}\"", self.escape_json_string(log_info.message)));
match log_info.file {
Some(file) => json_parts.push(format!("\"file\":\"{}\"", self.escape_json_string(file))),
None => json_parts.push("\"file\":null".to_string()),
}
match log_info.line {
Some(line) => json_parts.push(format!("\"line\":{}", line)),
None => json_parts.push("\"line\":null".to_string()),
}
match log_info.thread {
Some(thread) => json_parts.push(format!("\"thread\":\"{}\"", self.escape_json_string(thread))),
None => json_parts.push("\"thread\":null".to_string()),
}
format!("{{{}}}", json_parts.join(","))
}
fn escape_json_string(&self, input: &str) -> String {
input
.replace("\\", "\\\\") .replace("\"", "\\\"") .replace("\n", "\\n") .replace("\r", "\\r") .replace("\t", "\\t") }
fn ensure_directory_exists(&self, file_path: &Path) -> LoggerResult<()> {
if let Some(parent_dir) = file_path.parent() {
if !parent_dir.exists() {
fs::create_dir_all(parent_dir)
.map_err(|_| LoggerError::DirectoryCreationFailed {
path: parent_dir.display().to_string(),
reason: "Failed to create parent directories for JSON file".to_string(),
})?;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::format::LogInfo;
use tempfile::tempdir;
use std::fs;
#[test]
fn test_write_basic_json_entry() {
let temp_dir = tempdir().unwrap();
let json_path = temp_dir.path().join("test.json");
let log_info = LogInfo::new("Test message", LogLevel::Info, "2025-09-06 15:30:45");
let writer = JsonWriter::new();
let result = writer.write_log_entry(&log_info, &json_path);
assert!(result.is_ok());
let content = fs::read_to_string(&json_path).unwrap();
assert!(content.contains("\"message\":\"Test message\""));
assert!(content.contains("\"level\":\"INFO\""));
assert!(content.contains("\"timestamp\":\"2025-09-06 15:30:45\""));
}
#[test]
fn test_write_detailed_json_entry() {
let temp_dir = tempdir().unwrap();
let json_path = temp_dir.path().join("detailed.json");
let log_info = LogInfo::new("Detailed test", LogLevel::Debug, "2025-09-06 15:30:45")
.with_location("test.rs", 42)
.with_thread("main");
let writer = JsonWriter::new();
writer.write_log_entry(&log_info, &json_path).unwrap();
let content = fs::read_to_string(&json_path).unwrap();
assert!(content.contains("\"file\":\"test.rs\""));
assert!(content.contains("\"line\":42"));
assert!(content.contains("\"thread\":\"main\""));
}
#[test]
fn test_json_string_escaping() {
let writer = JsonWriter::new();
let result = writer.escape_json_string("Message with \"quotes\" and \n newline");
assert_eq!(result, "Message with \\\"quotes\\\" and \\n newline");
}
#[test]
fn test_multiple_json_entries() {
let temp_dir = tempdir().unwrap();
let json_path = temp_dir.path().join("multi.json");
let writer = JsonWriter::new();
let info1 = LogInfo::new("First message", LogLevel::Info, "2025-09-06 15:30:45");
let info2 = LogInfo::new("Second message", LogLevel::Warning, "2025-09-06 15:30:46");
writer.write_log_entry(&info1, &json_path).unwrap();
writer.write_log_entry(&info2, &json_path).unwrap();
let content = fs::read_to_string(&json_path).unwrap();
let lines: Vec<&str> = content.trim().split('\n').collect();
assert_eq!(lines.len(), 2);
assert!(lines[0].contains("First message"));
assert!(lines[1].contains("Second message"));
}
}