Skip to main content

ryo_app/spec_dsl/
parser.rs

1//! Spec DSL parser for YAML and TOML formats
2
3use std::path::Path;
4use thiserror::Error;
5
6use super::types::DomainSpec;
7
8/// Spec parsing error
9#[derive(Debug, Error)]
10pub enum SpecParseError {
11    #[error("Failed to read file: {0}")]
12    IoError(#[from] std::io::Error),
13
14    #[error("Failed to parse YAML: {0}")]
15    YamlError(#[from] serde_yaml::Error),
16
17    #[error("Failed to parse TOML: {0}")]
18    TomlError(#[from] toml::de::Error),
19
20    #[error("Unsupported file format: {0}")]
21    UnsupportedFormat(String),
22}
23
24/// Parse a spec from a YAML string
25pub fn parse_spec(content: &str) -> Result<DomainSpec, SpecParseError> {
26    serde_yaml::from_str(content).map_err(SpecParseError::from)
27}
28
29/// Parse a spec from a TOML string
30pub fn parse_spec_toml(content: &str) -> Result<DomainSpec, SpecParseError> {
31    toml::from_str(content).map_err(SpecParseError::from)
32}
33
34/// Parse a spec from a file (auto-detect format by extension)
35pub fn parse_spec_from_file(path: impl AsRef<Path>) -> Result<DomainSpec, SpecParseError> {
36    let path = path.as_ref();
37    let content = std::fs::read_to_string(path)?;
38
39    let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
40
41    match ext {
42        "yaml" | "yml" => parse_spec(&content),
43        "toml" => parse_spec_toml(&content),
44        _ => Err(SpecParseError::UnsupportedFormat(ext.to_string())),
45    }
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51
52    #[test]
53    fn test_parse_minimal_spec() {
54        let yaml = r#"
55project:
56  name: "test"
57
58modules: []
59entities: []
60"#;
61
62        let spec = parse_spec(yaml).unwrap();
63        assert_eq!(spec.project.name, "test");
64        assert!(spec.modules.is_empty());
65        assert!(spec.entities.is_empty());
66    }
67
68    #[test]
69    fn test_parse_with_entities() {
70        let yaml = r#"
71project:
72  name: "ecommerce"
73
74entities:
75  - name: User
76    module: user
77    fields:
78      - name: id
79        type: UserId
80      - name: name
81        type: String
82    derives: [Debug, Clone]
83"#;
84
85        let spec = parse_spec(yaml).unwrap();
86        assert_eq!(spec.entities.len(), 1);
87
88        let user = &spec.entities[0];
89        assert_eq!(user.name, "User");
90        assert_eq!(user.fields.len(), 2);
91        assert_eq!(user.derives, vec!["Debug", "Clone"]);
92    }
93
94    #[test]
95    fn test_parse_with_modules() {
96        let yaml = r#"
97project:
98  name: "test"
99
100modules:
101  - name: lib
102    is_pub: true
103    children:
104      - name: user
105        is_pub: true
106      - name: common
107        is_pub: true
108        children:
109          - name: types
110            is_pub: true
111"#;
112
113        let spec = parse_spec(yaml).unwrap();
114        assert_eq!(spec.modules.len(), 1);
115
116        let lib = &spec.modules[0];
117        assert_eq!(lib.name, "lib");
118        assert_eq!(lib.children.len(), 2);
119
120        // Flatten and check
121        let flattened = lib.flatten("");
122        assert_eq!(flattened.len(), 4);
123    }
124
125    #[test]
126    fn test_parse_with_implementations() {
127        let yaml = r#"
128project:
129  name: "test"
130
131implementations:
132  - target: UserId
133    methods:
134      - name: new
135        self_param: null
136        return_type: Self
137        body: "Self(uuid::Uuid::new_v4())"
138        is_pub: true
139      - name: as_str
140        self_param: ref
141        return_type: "&str"
142        body: "self.0.as_str()"
143        is_pub: true
144"#;
145
146        let spec = parse_spec(yaml).unwrap();
147        assert_eq!(spec.implementations.len(), 1);
148
149        let impl_spec = &spec.implementations[0];
150        assert_eq!(impl_spec.target, "UserId");
151        assert_eq!(impl_spec.methods.len(), 2);
152
153        let new_method = &impl_spec.methods[0];
154        assert_eq!(new_method.name, "new");
155        assert!(new_method.self_param.is_none());
156        assert!(new_method.is_pub);
157    }
158
159    #[test]
160    fn test_parse_with_refactors() {
161        let yaml = r#"
162project:
163  name: "test"
164
165refactors:
166  - kind: AddBuilderPattern
167    targets:
168      - User
169      - Product
170
171  - kind: AddFromInto
172    pairs:
173      - [UserId, "uuid::Uuid"]
174      - [ProductId, "uuid::Uuid"]
175
176  - kind: OrganizeImports
177    target_modules: all
178"#;
179
180        let spec = parse_spec(yaml).unwrap();
181        assert_eq!(spec.refactors.len(), 3);
182    }
183
184    #[test]
185    fn test_parse_with_verification() {
186        let yaml = r#"
187project:
188  name: "test"
189
190verification:
191  phase1:
192    - kind: ModuleExists
193      paths: [user, product]
194
195  phase2:
196    - kind: TypeExists
197      types: [UserId, ProductId]
198
199  final:
200    - kind: Compiles
201"#;
202
203        let spec = parse_spec(yaml).unwrap();
204        let verification = spec.verification.as_ref().unwrap();
205
206        assert_eq!(verification.phase1.len(), 1);
207        assert_eq!(verification.phase2.len(), 1);
208        assert_eq!(verification.final_.len(), 1);
209    }
210}