1use super::{Record, SeedError};
6use serde_json::Value;
7use std::collections::HashMap;
8use std::path::Path;
9
10#[derive(Debug, Clone)]
12pub struct Reference {
13 pub field: String,
15 pub target_table: String,
17 pub target_index: usize,
19 pub target_field: String,
21}
22
23#[derive(Debug, Clone)]
25pub struct FixtureTemplate {
26 pub table: String,
28 pub records: Vec<Record>,
30 pub count: usize,
32 pub references: Vec<Reference>,
34 pub extends: Option<String>,
36}
37
38pub struct FixtureLoader;
40
41impl FixtureLoader {
42 pub fn load(path: &str) -> Result<FixtureTemplate, SeedError> {
44 let content = std::fs::read_to_string(path)?;
45 let ext = Path::new(path)
46 .extension()
47 .and_then(|e| e.to_str())
48 .unwrap_or("");
49 match ext {
50 "yaml" | "yml" => Self::parse_yaml(&content, path),
51 "json" => Self::parse_json(&content, path),
52 _ => Err(SeedError::FixtureParseFailed {
53 path: path.to_string(),
54 reason: format!("unsupported file extension: {}", ext),
55 }),
56 }
57 }
58
59 fn parse_yaml(content: &str, path: &str) -> Result<FixtureTemplate, SeedError> {
60 let yaml: serde_yaml::Value =
61 serde_yaml::from_str(content).map_err(|e| SeedError::FixtureParseFailed {
62 path: path.to_string(),
63 reason: e.to_string(),
64 })?;
65 Self::build_template(yaml, path)
66 }
67
68 fn parse_json(content: &str, path: &str) -> Result<FixtureTemplate, SeedError> {
69 let json: serde_json::Value =
70 serde_json::from_str(content).map_err(|e| SeedError::FixtureParseFailed {
71 path: path.to_string(),
72 reason: e.to_string(),
73 })?;
74 let yaml = serde_yaml::to_value(&json).map_err(|e| SeedError::FixtureParseFailed {
75 path: path.to_string(),
76 reason: e.to_string(),
77 })?;
78 Self::build_template(yaml, path)
79 }
80
81 fn build_template(yaml: serde_yaml::Value, path: &str) -> Result<FixtureTemplate, SeedError> {
82 let map = yaml
83 .as_mapping()
84 .ok_or_else(|| SeedError::FixtureParseFailed {
85 path: path.to_string(),
86 reason: "root must be a mapping".to_string(),
87 })?;
88 let table = map
89 .get(serde_yaml::Value::String("table".to_string()))
90 .and_then(|v| v.as_str())
91 .ok_or_else(|| SeedError::FixtureParseFailed {
92 path: path.to_string(),
93 reason: "missing 'table' field".to_string(),
94 })?
95 .to_string();
96 let count = map
97 .get(serde_yaml::Value::String("count".to_string()))
98 .and_then(|v| v.as_u64())
99 .unwrap_or(0) as usize;
100 let extends = map
101 .get(serde_yaml::Value::String("extends".to_string()))
102 .and_then(|v| v.as_str())
103 .map(|s| s.to_string());
104 let records = Self::extract_records(map, path)?;
105 let references = Self::extract_references(map);
106 Ok(FixtureTemplate {
107 table,
108 records,
109 count,
110 references,
111 extends,
112 })
113 }
114
115 fn extract_records(map: &serde_yaml::Mapping, path: &str) -> Result<Vec<Record>, SeedError> {
116 let fields = map.get(serde_yaml::Value::String("fields".to_string()));
117 let count = map
118 .get(serde_yaml::Value::String("count".to_string()))
119 .and_then(|v| v.as_u64())
120 .unwrap_or(1) as usize;
121 match fields {
122 Some(serde_yaml::Value::Mapping(field_map)) => {
123 let mut records = Vec::with_capacity(count);
124 for _ in 0..count {
125 let mut record = serde_json::Map::new();
126 for (k, v) in field_map {
127 let key = k.as_str().unwrap_or("").to_string();
128 let value = serde_json::to_value(v).unwrap_or(Value::Null);
129 record.insert(key, value);
130 }
131 records.push(record);
132 }
133 Ok(records)
134 }
135 Some(serde_yaml::Value::Sequence(items)) => {
136 let mut records = Vec::with_capacity(items.len());
137 for item in items {
138 if let Some(item_map) = item.as_mapping() {
139 let mut record = serde_json::Map::new();
140 for (k, v) in item_map {
141 let key = k.as_str().unwrap_or("").to_string();
142 let value = serde_json::to_value(v).unwrap_or(Value::Null);
143 record.insert(key, value);
144 }
145 records.push(record);
146 }
147 }
148 Ok(records)
149 }
150 None => Ok(Vec::new()),
151 _ => Err(SeedError::FixtureParseFailed {
152 path: path.to_string(),
153 reason: "fields must be a mapping or sequence".to_string(),
154 }),
155 }
156 }
157
158 fn extract_references(map: &serde_yaml::Mapping) -> Vec<Reference> {
159 let refs = map.get(serde_yaml::Value::String("references".to_string()));
160 match refs {
161 Some(serde_yaml::Value::Sequence(items)) => items
162 .iter()
163 .filter_map(|item| {
164 let m = item.as_mapping()?;
165 let field = m
166 .get(serde_yaml::Value::String("field".to_string()))?
167 .as_str()?
168 .to_string();
169 let target_table = m
170 .get(serde_yaml::Value::String("target".to_string()))?
171 .as_str()?
172 .to_string();
173 let target_index = m
174 .get(serde_yaml::Value::String("index".to_string()))?
175 .as_u64()? as usize;
176 let target_field = m
177 .get(serde_yaml::Value::String("target_field".to_string()))?
178 .as_str()?
179 .to_string();
180 Some(Reference {
181 field,
182 target_table,
183 target_index,
184 target_field,
185 })
186 })
187 .collect(),
188 _ => Vec::new(),
189 }
190 }
191
192 pub fn resolve_references(
194 template: &mut FixtureTemplate,
195 resolved: &HashMap<String, Vec<Record>>,
196 ) -> Result<(), SeedError> {
197 for reference in &template.references {
198 let target_records = resolved.get(&reference.target_table).ok_or_else(|| {
199 SeedError::InvalidConfig(format!(
200 "reference target table '{}' not found",
201 reference.target_table
202 ))
203 })?;
204 let target_record = target_records.get(reference.target_index).ok_or_else(|| {
205 SeedError::InvalidConfig(format!(
206 "reference target index {} out of range",
207 reference.target_index
208 ))
209 })?;
210 let target_value = target_record
211 .get(&reference.target_field)
212 .cloned()
213 .unwrap_or(Value::Null);
214 for record in &mut template.records {
215 record.insert(reference.field.clone(), target_value.clone());
216 }
217 }
218 Ok(())
219 }
220
221 pub fn load_dir(dir: &str) -> Result<Vec<FixtureTemplate>, SeedError> {
223 let mut templates = Vec::new();
224 let path = Path::new(dir);
225 if !path.exists() {
226 return Err(SeedError::Io(std::io::Error::new(
227 std::io::ErrorKind::NotFound,
228 format!("directory not found: {}", dir),
229 )));
230 }
231 let mut entries: Vec<_> = std::fs::read_dir(path)?
232 .filter_map(|e| e.ok())
233 .filter(|e| {
234 e.path()
235 .extension()
236 .and_then(|ext| ext.to_str())
237 .is_some_and(|ext| ext == "yaml" || ext == "yml" || ext == "json")
238 })
239 .collect();
240 entries.sort_by_key(|e| e.path());
241 for entry in entries {
242 let path_str = entry.path().to_string_lossy().to_string();
243 templates.push(Self::load(&path_str)?);
244 }
245 Ok(templates)
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252 use serde_json::json;
253 use std::io::Write;
254
255 fn write_temp_file(name: &str, content: &str) -> String {
256 let dir = std::env::temp_dir();
257 let path = dir.join(format!("sz_orm_fixture_{}", name));
258 let mut file = std::fs::File::create(&path).unwrap();
259 file.write_all(content.as_bytes()).unwrap();
260 path.to_string_lossy().to_string()
261 }
262
263 fn cleanup(path: &str) {
264 let _ = std::fs::remove_file(path);
265 }
266
267 #[test]
268 fn test_load_yaml_fixture() {
269 let content = r#"
270table: users
271count: 3
272fields:
273 name: "张三"
274 email: "zhangsan@example.com"
275 age: 30
276"#;
277 let path = write_temp_file("test1.yaml", content);
278 let template = FixtureLoader::load(&path).unwrap();
279 assert_eq!(template.table, "users");
280 assert_eq!(template.count, 3);
281 assert_eq!(template.records.len(), 3);
282 assert_eq!(template.records[0]["name"], "张三");
283 cleanup(&path);
284 }
285
286 #[test]
287 fn test_load_json_fixture() {
288 let content =
289 r#"{"table": "orders", "count": 2, "fields": {"order_id": 1001, "amount": 99.9}}"#;
290 let path = write_temp_file("test2.json", content);
291 let template = FixtureLoader::load(&path).unwrap();
292 assert_eq!(template.table, "orders");
293 assert_eq!(template.records.len(), 2);
294 cleanup(&path);
295 }
296
297 #[test]
298 fn test_resolve_references() {
299 let mut template = FixtureTemplate {
300 table: "orders".to_string(),
301 records: vec![serde_json::Map::new()],
302 count: 1,
303 references: vec![Reference {
304 field: "user_id".to_string(),
305 target_table: "users".to_string(),
306 target_index: 0,
307 target_field: "id".to_string(),
308 }],
309 extends: None,
310 };
311 let mut user_record = serde_json::Map::new();
312 user_record.insert("id".to_string(), json!(42));
313 let resolved: HashMap<String, Vec<Record>> = vec![("users".to_string(), vec![user_record])]
314 .into_iter()
315 .collect();
316 FixtureLoader::resolve_references(&mut template, &resolved).unwrap();
317 assert_eq!(template.records[0]["user_id"], json!(42));
318 }
319
320 #[test]
321 fn test_parse_error() {
322 let path = write_temp_file("test3.yaml", "invalid: yaml: content: [");
323 let result = FixtureLoader::load(&path);
324 assert!(result.is_err());
325 let err = result.unwrap_err();
326 assert!(matches!(err, SeedError::FixtureParseFailed { .. }));
327 cleanup(&path);
328 }
329
330 #[test]
331 fn test_unsupported_extension() {
332 let path = write_temp_file("test4.txt", "content");
333 let result = FixtureLoader::load(&path);
334 assert!(result.is_err());
335 cleanup(&path);
336 }
337}