use super::{Record, SeedError};
use serde_json::Value;
use std::collections::HashMap;
use std::path::Path;
#[derive(Debug, Clone)]
pub struct Reference {
pub field: String,
pub target_table: String,
pub target_index: usize,
pub target_field: String,
}
#[derive(Debug, Clone)]
pub struct FixtureTemplate {
pub table: String,
pub records: Vec<Record>,
pub count: usize,
pub references: Vec<Reference>,
pub extends: Option<String>,
}
pub struct FixtureLoader;
impl FixtureLoader {
pub fn load(path: &str) -> Result<FixtureTemplate, SeedError> {
let content = std::fs::read_to_string(path)?;
let ext = Path::new(path)
.extension()
.and_then(|e| e.to_str())
.unwrap_or("");
match ext {
"yaml" | "yml" => Self::parse_yaml(&content, path),
"json" => Self::parse_json(&content, path),
_ => Err(SeedError::FixtureParseFailed {
path: path.to_string(),
reason: format!("unsupported file extension: {}", ext),
}),
}
}
fn parse_yaml(content: &str, path: &str) -> Result<FixtureTemplate, SeedError> {
let yaml: serde_yaml::Value =
serde_yaml::from_str(content).map_err(|e| SeedError::FixtureParseFailed {
path: path.to_string(),
reason: e.to_string(),
})?;
Self::build_template(yaml, path)
}
fn parse_json(content: &str, path: &str) -> Result<FixtureTemplate, SeedError> {
let json: serde_json::Value =
serde_json::from_str(content).map_err(|e| SeedError::FixtureParseFailed {
path: path.to_string(),
reason: e.to_string(),
})?;
let yaml = serde_yaml::to_value(&json).map_err(|e| SeedError::FixtureParseFailed {
path: path.to_string(),
reason: e.to_string(),
})?;
Self::build_template(yaml, path)
}
fn build_template(yaml: serde_yaml::Value, path: &str) -> Result<FixtureTemplate, SeedError> {
let map = yaml
.as_mapping()
.ok_or_else(|| SeedError::FixtureParseFailed {
path: path.to_string(),
reason: "root must be a mapping".to_string(),
})?;
let table = map
.get(serde_yaml::Value::String("table".to_string()))
.and_then(|v| v.as_str())
.ok_or_else(|| SeedError::FixtureParseFailed {
path: path.to_string(),
reason: "missing 'table' field".to_string(),
})?
.to_string();
let count = map
.get(serde_yaml::Value::String("count".to_string()))
.and_then(|v| v.as_u64())
.unwrap_or(0) as usize;
let extends = map
.get(serde_yaml::Value::String("extends".to_string()))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let records = Self::extract_records(map, path)?;
let references = Self::extract_references(map);
Ok(FixtureTemplate {
table,
records,
count,
references,
extends,
})
}
fn extract_records(map: &serde_yaml::Mapping, path: &str) -> Result<Vec<Record>, SeedError> {
let fields = map.get(serde_yaml::Value::String("fields".to_string()));
let count = map
.get(serde_yaml::Value::String("count".to_string()))
.and_then(|v| v.as_u64())
.unwrap_or(1) as usize;
match fields {
Some(serde_yaml::Value::Mapping(field_map)) => {
let mut records = Vec::with_capacity(count);
for _ in 0..count {
let mut record = serde_json::Map::new();
for (k, v) in field_map {
let key = k.as_str().unwrap_or("").to_string();
let value = serde_json::to_value(v).unwrap_or(Value::Null);
record.insert(key, value);
}
records.push(record);
}
Ok(records)
}
Some(serde_yaml::Value::Sequence(items)) => {
let mut records = Vec::with_capacity(items.len());
for item in items {
if let Some(item_map) = item.as_mapping() {
let mut record = serde_json::Map::new();
for (k, v) in item_map {
let key = k.as_str().unwrap_or("").to_string();
let value = serde_json::to_value(v).unwrap_or(Value::Null);
record.insert(key, value);
}
records.push(record);
}
}
Ok(records)
}
None => Ok(Vec::new()),
_ => Err(SeedError::FixtureParseFailed {
path: path.to_string(),
reason: "fields must be a mapping or sequence".to_string(),
}),
}
}
fn extract_references(map: &serde_yaml::Mapping) -> Vec<Reference> {
let refs = map.get(serde_yaml::Value::String("references".to_string()));
match refs {
Some(serde_yaml::Value::Sequence(items)) => items
.iter()
.filter_map(|item| {
let m = item.as_mapping()?;
let field = m
.get(serde_yaml::Value::String("field".to_string()))?
.as_str()?
.to_string();
let target_table = m
.get(serde_yaml::Value::String("target".to_string()))?
.as_str()?
.to_string();
let target_index = m
.get(serde_yaml::Value::String("index".to_string()))?
.as_u64()? as usize;
let target_field = m
.get(serde_yaml::Value::String("target_field".to_string()))?
.as_str()?
.to_string();
Some(Reference {
field,
target_table,
target_index,
target_field,
})
})
.collect(),
_ => Vec::new(),
}
}
pub fn resolve_references(
template: &mut FixtureTemplate,
resolved: &HashMap<String, Vec<Record>>,
) -> Result<(), SeedError> {
for reference in &template.references {
let target_records = resolved.get(&reference.target_table).ok_or_else(|| {
SeedError::InvalidConfig(format!(
"reference target table '{}' not found",
reference.target_table
))
})?;
let target_record = target_records.get(reference.target_index).ok_or_else(|| {
SeedError::InvalidConfig(format!(
"reference target index {} out of range",
reference.target_index
))
})?;
let target_value = target_record
.get(&reference.target_field)
.cloned()
.unwrap_or(Value::Null);
for record in &mut template.records {
record.insert(reference.field.clone(), target_value.clone());
}
}
Ok(())
}
pub fn load_dir(dir: &str) -> Result<Vec<FixtureTemplate>, SeedError> {
let mut templates = Vec::new();
let path = Path::new(dir);
if !path.exists() {
return Err(SeedError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("directory not found: {}", dir),
)));
}
let mut entries: Vec<_> = std::fs::read_dir(path)?
.filter_map(|e| e.ok())
.filter(|e| {
e.path()
.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| ext == "yaml" || ext == "yml" || ext == "json")
})
.collect();
entries.sort_by_key(|e| e.path());
for entry in entries {
let path_str = entry.path().to_string_lossy().to_string();
templates.push(Self::load(&path_str)?);
}
Ok(templates)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use std::io::Write;
fn write_temp_file(name: &str, content: &str) -> String {
let dir = std::env::temp_dir();
let path = dir.join(format!("sz_orm_fixture_{}", name));
let mut file = std::fs::File::create(&path).unwrap();
file.write_all(content.as_bytes()).unwrap();
path.to_string_lossy().to_string()
}
fn cleanup(path: &str) {
let _ = std::fs::remove_file(path);
}
#[test]
fn test_load_yaml_fixture() {
let content = r#"
table: users
count: 3
fields:
name: "张三"
email: "zhangsan@example.com"
age: 30
"#;
let path = write_temp_file("test1.yaml", content);
let template = FixtureLoader::load(&path).unwrap();
assert_eq!(template.table, "users");
assert_eq!(template.count, 3);
assert_eq!(template.records.len(), 3);
assert_eq!(template.records[0]["name"], "张三");
cleanup(&path);
}
#[test]
fn test_load_json_fixture() {
let content =
r#"{"table": "orders", "count": 2, "fields": {"order_id": 1001, "amount": 99.9}}"#;
let path = write_temp_file("test2.json", content);
let template = FixtureLoader::load(&path).unwrap();
assert_eq!(template.table, "orders");
assert_eq!(template.records.len(), 2);
cleanup(&path);
}
#[test]
fn test_resolve_references() {
let mut template = FixtureTemplate {
table: "orders".to_string(),
records: vec![serde_json::Map::new()],
count: 1,
references: vec![Reference {
field: "user_id".to_string(),
target_table: "users".to_string(),
target_index: 0,
target_field: "id".to_string(),
}],
extends: None,
};
let mut user_record = serde_json::Map::new();
user_record.insert("id".to_string(), json!(42));
let resolved: HashMap<String, Vec<Record>> = vec![("users".to_string(), vec![user_record])]
.into_iter()
.collect();
FixtureLoader::resolve_references(&mut template, &resolved).unwrap();
assert_eq!(template.records[0]["user_id"], json!(42));
}
#[test]
fn test_parse_error() {
let path = write_temp_file("test3.yaml", "invalid: yaml: content: [");
let result = FixtureLoader::load(&path);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, SeedError::FixtureParseFailed { .. }));
cleanup(&path);
}
#[test]
fn test_unsupported_extension() {
let path = write_temp_file("test4.txt", "content");
let result = FixtureLoader::load(&path);
assert!(result.is_err());
cleanup(&path);
}
}