use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileInfo {
pub lines: Vec<String>,
pub total_lines: usize,
pub total_lines_exact: bool,
pub total_bytes: usize,
pub truncated: bool,
pub truncated_by_lines: bool,
pub truncated_by_bytes: bool,
pub language: Option<String>,
pub encoding: String,
pub syntax_errors: Vec<String>,
pub compression_ratio: Option<f64>,
}
impl FileInfo {
pub fn new() -> Self {
Self {
lines: Vec::new(),
total_lines: 0,
total_lines_exact: true,
total_bytes: 0,
truncated: false,
truncated_by_lines: false,
truncated_by_bytes: false,
language: None,
encoding: "UTF-8".to_string(),
syntax_errors: Vec::new(),
compression_ratio: None,
}
}
pub const fn with_metadata(
total_lines: usize,
total_bytes: usize,
language: Option<String>,
encoding: String,
) -> Self {
Self {
lines: Vec::new(),
total_lines,
total_lines_exact: true,
total_bytes,
truncated: false,
truncated_by_lines: false,
truncated_by_bytes: false,
language,
encoding,
syntax_errors: Vec::new(),
compression_ratio: None,
}
}
pub fn with_lines(mut self, lines: Vec<String>) -> Self {
self.lines = lines;
self
}
pub const fn with_truncation(
mut self,
truncated: bool,
by_lines: bool,
by_bytes: bool,
) -> Self {
self.truncated = truncated;
self.truncated_by_lines = by_lines;
self.truncated_by_bytes = by_bytes;
self
}
pub fn add_syntax_error(&mut self, error: String) {
self.syntax_errors.push(error);
}
pub const fn with_compression_ratio(mut self, ratio: Option<f64>) -> Self {
self.compression_ratio = ratio;
self
}
pub const fn with_total_lines_exact(mut self, exact: bool) -> Self {
self.total_lines_exact = exact;
self
}
pub fn is_success(&self) -> bool {
self.syntax_errors.is_empty()
}
pub fn processed_lines(&self) -> usize {
self.lines.len()
}
pub fn processing_ratio(&self) -> f64 {
if self.total_lines == 0 {
1.0
} else {
self.processed_lines() as f64 / self.total_lines as f64
}
}
pub fn truncation_reason(&self) -> Option<String> {
if !self.truncated {
return None;
}
let mut reasons = Vec::new();
if self.truncated_by_lines {
reasons.push("line limit");
}
if self.truncated_by_bytes {
reasons.push("byte limit");
}
if reasons.is_empty() {
Some("unknown reason".to_string())
} else {
Some(reasons.join(" and "))
}
}
pub fn get_stats_summary(&self) -> ProcessingStats {
ProcessingStats {
total_lines: self.total_lines,
total_lines_exact: self.total_lines_exact,
processed_lines: self.processed_lines(),
total_bytes: self.total_bytes,
truncated: self.truncated,
truncation_reason: self.truncation_reason(),
has_syntax_errors: !self.syntax_errors.is_empty(),
error_count: self.syntax_errors.len(),
language: self.language.clone(),
encoding: self.encoding.clone(),
}
}
}
impl Default for FileInfo {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProcessingStats {
pub total_lines: usize,
pub total_lines_exact: bool,
pub processed_lines: usize,
pub total_bytes: usize,
pub truncated: bool,
pub truncation_reason: Option<String>,
pub has_syntax_errors: bool,
pub error_count: usize,
pub language: Option<String>,
pub encoding: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_file_info() {
let info = FileInfo::new();
assert_eq!(info.lines.len(), 0);
assert_eq!(info.total_lines, 0);
assert!(info.total_lines_exact);
assert_eq!(info.total_bytes, 0);
assert!(!info.truncated);
assert!(!info.truncated_by_lines);
assert!(!info.truncated_by_bytes);
assert_eq!(info.language, None);
assert_eq!(info.encoding, "UTF-8");
assert_eq!(info.syntax_errors.len(), 0);
}
#[test]
fn test_with_metadata() {
let info =
FileInfo::with_metadata(100, 1024, Some("rust".to_string()), "UTF-8".to_string());
assert_eq!(info.total_lines, 100);
assert!(info.total_lines_exact);
assert_eq!(info.total_bytes, 1024);
assert_eq!(info.language, Some("rust".to_string()));
assert_eq!(info.encoding, "UTF-8");
}
#[test]
fn test_builder_pattern() {
let lines = vec!["line1".to_string(), "line2".to_string()];
let info = FileInfo::new()
.with_lines(lines.clone())
.with_truncation(true, true, false)
.with_total_lines_exact(false);
assert_eq!(info.lines, lines);
assert!(info.truncated);
assert!(info.truncated_by_lines);
assert!(!info.truncated_by_bytes);
assert!(!info.total_lines_exact);
}
#[test]
#[allow(clippy::float_cmp)]
fn test_processing_ratio() {
let mut info = FileInfo::new();
info.total_lines = 100;
info.lines = vec!["line".to_string(); 50];
assert_eq!(info.processing_ratio(), 0.5);
info.total_lines = 0;
assert_eq!(info.processing_ratio(), 1.0);
}
#[test]
fn test_truncation_reason() {
let mut info = FileInfo::new();
assert_eq!(info.truncation_reason(), None);
info.truncated = true;
info.truncated_by_lines = true;
assert_eq!(info.truncation_reason(), Some("line limit".to_string()));
info.truncated_by_bytes = true;
assert_eq!(
info.truncation_reason(),
Some("line limit and byte limit".to_string())
);
info.truncated_by_lines = false;
assert_eq!(info.truncation_reason(), Some("byte limit".to_string()));
}
#[test]
fn test_helper_methods() {
let mut info = FileInfo::new();
assert!(info.is_success());
info.add_syntax_error("test error".to_string());
assert!(!info.is_success());
}
#[test]
fn test_stats_summary() {
let mut info =
FileInfo::with_metadata(100, 2048, Some("rust".to_string()), "UTF-8".to_string());
info.lines = vec!["line".to_string(); 50];
info.truncated = true;
info.truncated_by_lines = true;
info.total_lines_exact = false;
info.add_syntax_error("test error".to_string());
let stats = info.get_stats_summary();
assert_eq!(stats.total_lines, 100);
assert!(!stats.total_lines_exact);
assert_eq!(stats.processed_lines, 50);
assert_eq!(stats.total_bytes, 2048);
assert!(stats.truncated);
assert_eq!(stats.truncation_reason, Some("line limit".to_string()));
assert!(stats.has_syntax_errors);
assert_eq!(stats.error_count, 1);
assert_eq!(stats.language, Some("rust".to_string()));
assert_eq!(stats.encoding, "UTF-8");
}
}