1use crate::error::SynthError;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct GenerationContext {
13 pub seed: u64,
15 pub fiscal_year: u32,
17 pub company_code: String,
19 pub industry: String,
21 #[serde(default)]
23 pub extra: HashMap<String, String>,
24}
25
26impl GenerationContext {
27 pub fn new(seed: u64, fiscal_year: u32, company_code: impl Into<String>) -> Self {
29 Self {
30 seed,
31 fiscal_year,
32 company_code: company_code.into(),
33 industry: String::new(),
34 extra: HashMap::new(),
35 }
36 }
37
38 pub fn with_industry(mut self, industry: impl Into<String>) -> Self {
40 self.industry = industry.into();
41 self
42 }
43
44 pub fn with_extra(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
46 self.extra.insert(key.into(), value.into());
47 self
48 }
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct GeneratedRecord {
54 pub record_type: String,
56 pub fields: HashMap<String, serde_json::Value>,
58}
59
60impl GeneratedRecord {
61 pub fn new(record_type: impl Into<String>) -> Self {
63 Self {
64 record_type: record_type.into(),
65 fields: HashMap::new(),
66 }
67 }
68
69 pub fn with_field(
71 mut self,
72 key: impl Into<String>,
73 value: impl Into<serde_json::Value>,
74 ) -> Self {
75 self.fields.insert(key.into(), value.into());
76 self
77 }
78
79 pub fn get(&self, key: &str) -> Option<&serde_json::Value> {
81 self.fields.get(key)
82 }
83
84 pub fn get_str(&self, key: &str) -> Option<&str> {
86 self.fields.get(key).and_then(|v| v.as_str())
87 }
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct SinkSummary {
93 pub records_written: usize,
95 pub bytes_written: Option<u64>,
97 pub file_paths: Vec<String>,
99 #[serde(default)]
101 pub metadata: HashMap<String, String>,
102}
103
104impl SinkSummary {
105 pub fn new(records_written: usize) -> Self {
107 Self {
108 records_written,
109 bytes_written: None,
110 file_paths: Vec::new(),
111 metadata: HashMap::new(),
112 }
113 }
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct PluginInfo {
119 pub name: String,
121 pub version: String,
123 pub description: String,
125 pub plugin_type: PluginType,
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
131#[serde(rename_all = "snake_case")]
132pub enum PluginType {
133 Generator,
135 Sink,
137 Transform,
139}
140
141pub trait GeneratorPlugin: Send + Sync {
168 fn name(&self) -> &str;
170 fn version(&self) -> &str;
172 fn description(&self) -> &str;
174 fn config_schema(&self) -> Option<serde_json::Value>;
176 fn generate(
178 &self,
179 config: &serde_json::Value,
180 context: &GenerationContext,
181 ) -> Result<Vec<GeneratedRecord>, SynthError>;
182}
183
184pub trait SinkPlugin: Send + Sync {
194 fn name(&self) -> &str;
196 fn initialize(&mut self, config: &serde_json::Value) -> Result<(), SynthError>;
198 fn write_records(&mut self, records: &[GeneratedRecord]) -> Result<usize, SynthError>;
200 fn finalize(&mut self) -> Result<SinkSummary, SynthError>;
202}
203
204pub trait TransformPlugin: Send + Sync {
208 fn name(&self) -> &str;
210 fn transform(&self, records: Vec<GeneratedRecord>) -> Result<Vec<GeneratedRecord>, SynthError>;
212}
213
214#[cfg(test)]
215#[allow(clippy::unwrap_used)]
216mod tests {
217 use super::*;
218
219 #[test]
220 fn test_generation_context_creation() {
221 let ctx = GenerationContext::new(42, 2024, "C001")
222 .with_industry("manufacturing")
223 .with_extra("region", "EU");
224 assert_eq!(ctx.seed, 42);
225 assert_eq!(ctx.fiscal_year, 2024);
226 assert_eq!(ctx.company_code, "C001");
227 assert_eq!(ctx.industry, "manufacturing");
228 assert_eq!(ctx.extra.get("region").map(|s| s.as_str()), Some("EU"));
229 }
230
231 #[test]
232 fn test_generated_record_creation() {
233 let record = GeneratedRecord::new("test_record")
234 .with_field("name", serde_json::Value::String("Test".to_string()))
235 .with_field("amount", serde_json::json!(100.0));
236 assert_eq!(record.record_type, "test_record");
237 assert_eq!(record.get_str("name"), Some("Test"));
238 assert!(record.get("amount").is_some());
239 }
240
241 #[test]
242 fn test_generated_record_serialization() {
243 let record = GeneratedRecord::new("vendor")
244 .with_field("id", serde_json::json!("V001"))
245 .with_field("name", serde_json::json!("Acme Corp"));
246 let json = serde_json::to_string(&record).expect("should serialize");
247 let deser: GeneratedRecord = serde_json::from_str(&json).expect("should deserialize");
248 assert_eq!(deser.record_type, "vendor");
249 assert_eq!(deser.get_str("id"), Some("V001"));
250 }
251
252 #[test]
253 fn test_sink_summary_creation() {
254 let summary = SinkSummary::new(100);
255 assert_eq!(summary.records_written, 100);
256 assert!(summary.bytes_written.is_none());
257 assert!(summary.file_paths.is_empty());
258 }
259
260 #[test]
261 fn test_plugin_info_serialization() {
262 let info = PluginInfo {
263 name: "test_plugin".to_string(),
264 version: "1.0.0".to_string(),
265 description: "A test plugin".to_string(),
266 plugin_type: PluginType::Generator,
267 };
268 let json = serde_json::to_string(&info).expect("should serialize");
269 assert!(json.contains("test_plugin"));
270 assert!(json.contains("generator"));
271 }
272}