use chrono::Utc;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::sync::RwLock;
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FileType {
Pdf,
Csv,
Json,
Epcis,
}
impl FileType {
pub fn extension(&self) -> &'static str {
match self {
FileType::Pdf => "pdf",
FileType::Csv => "csv",
FileType::Json => "json",
FileType::Epcis => "xml",
}
}
pub fn mime_type(&self) -> &'static str {
match self {
FileType::Pdf => "application/pdf",
FileType::Csv => "text/csv",
FileType::Json => "application/json",
FileType::Epcis => "application/xml",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"pdf" => Some(FileType::Pdf),
"csv" => Some(FileType::Csv),
"json" => Some(FileType::Json),
"epcis" | "xml" => Some(FileType::Epcis),
_ => None,
}
}
}
impl std::str::FromStr for FileType {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s).ok_or_else(|| format!("Unknown file type: {s}"))
}
}
#[derive(Debug, Clone)]
pub struct GenerationContext {
pub route_id: String,
pub file_type: FileType,
pub request_id: Option<String>,
pub metadata: serde_json::Value,
}
#[derive(Debug, Clone)]
pub struct FileGenerator {
base_dir: PathBuf,
stats: Arc<RwLock<GenerationStats>>,
}
#[derive(Debug, Default)]
struct GenerationStats {
files_generated: u64,
total_bytes: u64,
}
impl FileGenerator {
pub fn new(base_dir: impl AsRef<Path>) -> Self {
let base_dir = base_dir.as_ref().to_path_buf();
Self {
base_dir,
stats: Arc::new(RwLock::new(GenerationStats::default())),
}
}
pub async fn generate_file(&self, context: GenerationContext) -> anyhow::Result<PathBuf> {
let route_dir = self.base_dir.join(&context.route_id);
tokio::fs::create_dir_all(&route_dir).await?;
let filename = format!(
"{}_{}.{}",
context.request_id.as_ref().unwrap_or(&Uuid::new_v4().to_string()),
Utc::now().timestamp(),
context.file_type.extension()
);
let file_path = route_dir.join(&filename);
let content = match context.file_type {
FileType::Pdf => self.generate_pdf(&context)?,
FileType::Csv => self.generate_csv(&context)?,
FileType::Json => self.generate_json(&context)?,
FileType::Epcis => self.generate_epcis(&context)?,
};
tokio::fs::write(&file_path, content).await?;
{
let mut stats = self.stats.write().await;
stats.files_generated += 1;
stats.total_bytes += file_path.metadata()?.len();
}
tracing::debug!("Generated file: {:?}", file_path);
Ok(file_path)
}
fn generate_pdf(&self, context: &GenerationContext) -> anyhow::Result<Vec<u8>> {
let _title = format!("Mock {} Document", context.route_id);
let content = format!(
"MockForge Generated Document\n\
Route: {}\n\
Generated: {}\n\
Request ID: {}\n\
\n\
This is a mock document generated by MockForge.\n\
In a real implementation, this would contain actual data.\n",
context.route_id,
Utc::now().to_rfc3339(),
context.request_id.as_deref().unwrap_or("N/A")
);
let pdf_content = format!(
"%PDF-1.4\n\
1 0 obj\n\
<< /Type /Catalog /Pages 2 0 R >>\n\
endobj\n\
2 0 obj\n\
<< /Type /Pages /Kids [3 0 R] /Count 1 >>\n\
endobj\n\
3 0 obj\n\
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R >>\n\
endobj\n\
4 0 obj\n\
<< /Length {} >>\n\
stream\n\
BT\n\
/F1 12 Tf\n\
100 700 Td\n\
({}) Tj\n\
ET\n\
endstream\n\
endobj\n\
xref\n\
0 5\n\
0000000000 65535 f\n\
0000000009 00000 n\n\
0000000058 00000 n\n\
0000000115 00000 n\n\
0000000215 00000 n\n\
trailer\n\
<< /Size 5 /Root 1 0 R >>\n\
startxref\n\
{}\n\
%%EOF",
content.len(),
content.replace("(", "\\(").replace(")", "\\)"),
content.len() + 200 );
Ok(pdf_content.into_bytes())
}
fn generate_csv(&self, context: &GenerationContext) -> anyhow::Result<Vec<u8>> {
let mut csv = String::new();
csv.push_str("ID,Route,Generated At,Request ID\n");
csv.push_str(&format!(
"{},{},{},{}\n",
Uuid::new_v4(),
context.route_id,
Utc::now().to_rfc3339(),
context.request_id.as_deref().unwrap_or("N/A")
));
if let Some(metadata_array) = context.metadata.as_array() {
for item in metadata_array {
if let Some(obj) = item.as_object() {
let row: Vec<String> =
obj.values().map(|v| v.to_string().trim_matches('"').to_string()).collect();
csv.push_str(&row.join(","));
csv.push('\n');
}
}
}
Ok(csv.into_bytes())
}
fn generate_json(&self, context: &GenerationContext) -> anyhow::Result<Vec<u8>> {
let json = serde_json::json!({
"route_id": context.route_id,
"generated_at": Utc::now().to_rfc3339(),
"request_id": context.request_id,
"metadata": context.metadata,
"mockforge_version": env!("CARGO_PKG_VERSION"),
});
Ok(serde_json::to_vec_pretty(&json)?)
}
fn generate_epcis(&self, context: &GenerationContext) -> anyhow::Result<Vec<u8>> {
let xml = format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<epcis:EPCISDocument xmlns:epcis="urn:epcglobal:epcis:xsd:1" xmlns:cbv="urn:epcglobal:cbv:mda" xmlns:gdst="https://ref.gs1.org/cbv/">
<EPCISHeader>
<epcis:version>2.0</epcis:version>
</EPCISHeader>
<EPCISBody>
<EventList>
<ObjectEvent>
<eventTime>{}</eventTime>
<eventTimeZoneOffset>+00:00</eventTimeZoneOffset>
<epcList>
<epc>{}</epc>
</epcList>
<action>OBSERVE</action>
<bizStep>urn:epcglobal:cbv:bizstep:receiving</bizStep>
<disposition>urn:epcglobal:cbv:disp:in_transit</disposition>
</ObjectEvent>
</EventList>
</EPCISBody>
</epcis:EPCISDocument>"#,
Utc::now().to_rfc3339(),
context.request_id.as_deref().unwrap_or(&Uuid::new_v4().to_string())
);
Ok(xml.into_bytes())
}
pub fn get_file_path(&self, route_id: &str, filename: &str) -> PathBuf {
self.base_dir.join(route_id).join(filename)
}
pub async fn get_stats(&self) -> (u64, u64) {
let stats = self.stats.read().await;
(stats.files_generated, stats.total_bytes)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[tokio::test]
async fn test_generate_pdf() {
let temp_dir = TempDir::new().unwrap();
let generator = FileGenerator::new(temp_dir.path());
let context = GenerationContext {
route_id: "labels".to_string(),
file_type: FileType::Pdf,
request_id: Some("test-123".to_string()),
metadata: serde_json::json!({}),
};
let path = generator.generate_file(context).await.unwrap();
assert!(path.exists());
assert!(path.extension().unwrap() == "pdf");
}
#[tokio::test]
async fn test_generate_csv() {
let temp_dir = TempDir::new().unwrap();
let generator = FileGenerator::new(temp_dir.path());
let context = GenerationContext {
route_id: "invoices".to_string(),
file_type: FileType::Csv,
request_id: Some("invoice-456".to_string()),
metadata: serde_json::json!([]),
};
let path = generator.generate_file(context).await.unwrap();
assert!(path.exists());
assert!(path.extension().unwrap() == "csv");
}
#[tokio::test]
async fn test_generate_json() {
let temp_dir = TempDir::new().unwrap();
let generator = FileGenerator::new(temp_dir.path());
let context = GenerationContext {
route_id: "provenance".to_string(),
file_type: FileType::Json,
request_id: Some("provenance-789".to_string()),
metadata: serde_json::json!({"test": "data"}),
};
let path = generator.generate_file(context).await.unwrap();
assert!(path.exists());
assert!(path.extension().unwrap() == "json");
let content = tokio::fs::read_to_string(&path).await.unwrap();
let json: serde_json::Value = serde_json::from_str(&content).unwrap();
assert!(json.get("route_id").is_some());
}
}