use crate::error::SynthError;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GenerationContext {
pub seed: u64,
pub fiscal_year: u32,
pub company_code: String,
pub industry: String,
#[serde(default)]
pub extra: HashMap<String, String>,
}
impl GenerationContext {
pub fn new(seed: u64, fiscal_year: u32, company_code: impl Into<String>) -> Self {
Self {
seed,
fiscal_year,
company_code: company_code.into(),
industry: String::new(),
extra: HashMap::new(),
}
}
pub fn with_industry(mut self, industry: impl Into<String>) -> Self {
self.industry = industry.into();
self
}
pub fn with_extra(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.extra.insert(key.into(), value.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GeneratedRecord {
pub record_type: String,
pub fields: HashMap<String, serde_json::Value>,
}
impl GeneratedRecord {
pub fn new(record_type: impl Into<String>) -> Self {
Self {
record_type: record_type.into(),
fields: HashMap::new(),
}
}
pub fn with_field(
mut self,
key: impl Into<String>,
value: impl Into<serde_json::Value>,
) -> Self {
self.fields.insert(key.into(), value.into());
self
}
pub fn get(&self, key: &str) -> Option<&serde_json::Value> {
self.fields.get(key)
}
pub fn get_str(&self, key: &str) -> Option<&str> {
self.fields.get(key).and_then(|v| v.as_str())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SinkSummary {
pub records_written: usize,
pub bytes_written: Option<u64>,
pub file_paths: Vec<String>,
#[serde(default)]
pub metadata: HashMap<String, String>,
}
impl SinkSummary {
pub fn new(records_written: usize) -> Self {
Self {
records_written,
bytes_written: None,
file_paths: Vec::new(),
metadata: HashMap::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginInfo {
pub name: String,
pub version: String,
pub description: String,
pub plugin_type: PluginType,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum PluginType {
Generator,
Sink,
Transform,
}
pub trait GeneratorPlugin: Send + Sync {
fn name(&self) -> &str;
fn version(&self) -> &str;
fn description(&self) -> &str;
fn config_schema(&self) -> Option<serde_json::Value>;
fn generate(
&self,
config: &serde_json::Value,
context: &GenerationContext,
) -> Result<Vec<GeneratedRecord>, SynthError>;
}
pub trait SinkPlugin: Send + Sync {
fn name(&self) -> &str;
fn version(&self) -> &str {
"0.1.0"
}
fn description(&self) -> &str {
""
}
fn initialize(&mut self, config: &serde_json::Value) -> Result<(), SynthError>;
fn write_records(&mut self, records: &[GeneratedRecord]) -> Result<usize, SynthError>;
fn finalize(&mut self) -> Result<SinkSummary, SynthError>;
}
pub trait TransformPlugin: Send + Sync {
fn name(&self) -> &str;
fn version(&self) -> &str {
"0.1.0"
}
fn description(&self) -> &str {
""
}
fn transform(&self, records: Vec<GeneratedRecord>) -> Result<Vec<GeneratedRecord>, SynthError>;
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_generation_context_creation() {
let ctx = GenerationContext::new(42, 2024, "C001")
.with_industry("manufacturing")
.with_extra("region", "EU");
assert_eq!(ctx.seed, 42);
assert_eq!(ctx.fiscal_year, 2024);
assert_eq!(ctx.company_code, "C001");
assert_eq!(ctx.industry, "manufacturing");
assert_eq!(ctx.extra.get("region").map(|s| s.as_str()), Some("EU"));
}
#[test]
fn test_generated_record_creation() {
let record = GeneratedRecord::new("test_record")
.with_field("name", serde_json::Value::String("Test".to_string()))
.with_field("amount", serde_json::json!(100.0));
assert_eq!(record.record_type, "test_record");
assert_eq!(record.get_str("name"), Some("Test"));
assert!(record.get("amount").is_some());
}
#[test]
fn test_generated_record_serialization() {
let record = GeneratedRecord::new("vendor")
.with_field("id", serde_json::json!("V001"))
.with_field("name", serde_json::json!("Acme Corp"));
let json = serde_json::to_string(&record).expect("should serialize");
let deser: GeneratedRecord = serde_json::from_str(&json).expect("should deserialize");
assert_eq!(deser.record_type, "vendor");
assert_eq!(deser.get_str("id"), Some("V001"));
}
#[test]
fn test_sink_summary_creation() {
let summary = SinkSummary::new(100);
assert_eq!(summary.records_written, 100);
assert!(summary.bytes_written.is_none());
assert!(summary.file_paths.is_empty());
}
#[test]
fn test_plugin_info_serialization() {
let info = PluginInfo {
name: "test_plugin".to_string(),
version: "1.0.0".to_string(),
description: "A test plugin".to_string(),
plugin_type: PluginType::Generator,
};
let json = serde_json::to_string(&info).expect("should serialize");
assert!(json.contains("test_plugin"));
assert!(json.contains("generator"));
}
}