use std::path::Path;
use thiserror::Error;
use super::types::DomainSpec;
#[derive(Debug, Error)]
pub enum SpecParseError {
#[error("Failed to read file: {0}")]
IoError(#[from] std::io::Error),
#[error("Failed to parse YAML: {0}")]
YamlError(#[from] serde_yaml::Error),
#[error("Failed to parse TOML: {0}")]
TomlError(#[from] toml::de::Error),
#[error("Unsupported file format: {0}")]
UnsupportedFormat(String),
}
pub fn parse_spec(content: &str) -> Result<DomainSpec, SpecParseError> {
serde_yaml::from_str(content).map_err(SpecParseError::from)
}
pub fn parse_spec_toml(content: &str) -> Result<DomainSpec, SpecParseError> {
toml::from_str(content).map_err(SpecParseError::from)
}
pub fn parse_spec_from_file(path: impl AsRef<Path>) -> Result<DomainSpec, SpecParseError> {
let path = path.as_ref();
let content = std::fs::read_to_string(path)?;
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
match ext {
"yaml" | "yml" => parse_spec(&content),
"toml" => parse_spec_toml(&content),
_ => Err(SpecParseError::UnsupportedFormat(ext.to_string())),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_minimal_spec() {
let yaml = r#"
project:
name: "test"
modules: []
entities: []
"#;
let spec = parse_spec(yaml).unwrap();
assert_eq!(spec.project.name, "test");
assert!(spec.modules.is_empty());
assert!(spec.entities.is_empty());
}
#[test]
fn test_parse_with_entities() {
let yaml = r#"
project:
name: "ecommerce"
entities:
- name: User
module: user
fields:
- name: id
type: UserId
- name: name
type: String
derives: [Debug, Clone]
"#;
let spec = parse_spec(yaml).unwrap();
assert_eq!(spec.entities.len(), 1);
let user = &spec.entities[0];
assert_eq!(user.name, "User");
assert_eq!(user.fields.len(), 2);
assert_eq!(user.derives, vec!["Debug", "Clone"]);
}
#[test]
fn test_parse_with_modules() {
let yaml = r#"
project:
name: "test"
modules:
- name: lib
is_pub: true
children:
- name: user
is_pub: true
- name: common
is_pub: true
children:
- name: types
is_pub: true
"#;
let spec = parse_spec(yaml).unwrap();
assert_eq!(spec.modules.len(), 1);
let lib = &spec.modules[0];
assert_eq!(lib.name, "lib");
assert_eq!(lib.children.len(), 2);
let flattened = lib.flatten("");
assert_eq!(flattened.len(), 4);
}
#[test]
fn test_parse_with_implementations() {
let yaml = r#"
project:
name: "test"
implementations:
- target: UserId
methods:
- name: new
self_param: null
return_type: Self
body: "Self(uuid::Uuid::new_v4())"
is_pub: true
- name: as_str
self_param: ref
return_type: "&str"
body: "self.0.as_str()"
is_pub: true
"#;
let spec = parse_spec(yaml).unwrap();
assert_eq!(spec.implementations.len(), 1);
let impl_spec = &spec.implementations[0];
assert_eq!(impl_spec.target, "UserId");
assert_eq!(impl_spec.methods.len(), 2);
let new_method = &impl_spec.methods[0];
assert_eq!(new_method.name, "new");
assert!(new_method.self_param.is_none());
assert!(new_method.is_pub);
}
#[test]
fn test_parse_with_refactors() {
let yaml = r#"
project:
name: "test"
refactors:
- kind: AddBuilderPattern
targets:
- User
- Product
- kind: AddFromInto
pairs:
- [UserId, "uuid::Uuid"]
- [ProductId, "uuid::Uuid"]
- kind: OrganizeImports
target_modules: all
"#;
let spec = parse_spec(yaml).unwrap();
assert_eq!(spec.refactors.len(), 3);
}
#[test]
fn test_parse_with_verification() {
let yaml = r#"
project:
name: "test"
verification:
phase1:
- kind: ModuleExists
paths: [user, product]
phase2:
- kind: TypeExists
types: [UserId, ProductId]
final:
- kind: Compiles
"#;
let spec = parse_spec(yaml).unwrap();
let verification = spec.verification.as_ref().unwrap();
assert_eq!(verification.phase1.len(), 1);
assert_eq!(verification.phase2.len(), 1);
assert_eq!(verification.final_.len(), 1);
}
}