1use crate::error::SpecError;
4use crate::parsers::{MarkdownParser, YamlParser};
5use crate::validation::ValidationEngine;
6
7pub struct FormatConverter;
9
10impl FormatConverter {
11 pub fn yaml_to_markdown(yaml_content: &str) -> Result<String, SpecError> {
24 let spec = YamlParser::parse(yaml_content)?;
26
27 ValidationEngine::validate(&spec)?;
29
30 MarkdownParser::serialize(&spec)
32 }
33
34 pub fn markdown_to_yaml(markdown_content: &str) -> Result<String, SpecError> {
47 let spec = MarkdownParser::parse(markdown_content)?;
49
50 ValidationEngine::validate(&spec)?;
52
53 YamlParser::serialize(&spec)
55 }
56
57 pub fn convert(content: &str, from_format: &str, to_format: &str) -> Result<String, SpecError> {
73 let from_lower = from_format.to_lowercase();
74 let to_lower = to_format.to_lowercase();
75
76 match (from_lower.as_str(), to_lower.as_str()) {
77 ("yaml", "markdown") => Self::yaml_to_markdown(content),
78 ("markdown", "yaml") => Self::markdown_to_yaml(content),
79 ("yaml", "yaml") => {
80 let spec = YamlParser::parse(content)?;
82 ValidationEngine::validate(&spec)?;
83 YamlParser::serialize(&spec)
84 }
85 ("markdown", "markdown") => {
86 let spec = MarkdownParser::parse(content)?;
88 ValidationEngine::validate(&spec)?;
89 MarkdownParser::serialize(&spec)
90 }
91 _ => Err(SpecError::InvalidFormat(format!(
92 "Unsupported format conversion: {} to {}",
93 from_format, to_format
94 ))),
95 }
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102 use crate::models::*;
103 use chrono::Utc;
104
105 #[test]
106 fn test_yaml_to_markdown_conversion() {
107 let spec = Spec {
108 id: "test-spec".to_string(),
109 name: "Test Spec".to_string(),
110 version: "1.0".to_string(),
111 requirements: vec![],
112 design: None,
113 tasks: vec![],
114 metadata: SpecMetadata {
115 author: Some("Test Author".to_string()),
116 created_at: Utc::now(),
117 updated_at: Utc::now(),
118 phase: SpecPhase::Requirements,
119 status: SpecStatus::Draft,
120 },
121 inheritance: None,
122 };
123
124 let yaml = YamlParser::serialize(&spec).expect("Failed to serialize to YAML");
125 let markdown =
126 FormatConverter::yaml_to_markdown(&yaml).expect("Failed to convert YAML to Markdown");
127
128 assert!(markdown.contains("# Test Spec"));
129 assert!(markdown.contains("test-spec"));
130 assert!(markdown.contains("Test Author"));
131 }
132
133 #[test]
134 fn test_markdown_to_yaml_conversion() {
135 let spec = Spec {
136 id: "test-spec".to_string(),
137 name: "Test Spec".to_string(),
138 version: "1.0".to_string(),
139 requirements: vec![],
140 design: None,
141 tasks: vec![],
142 metadata: SpecMetadata {
143 author: Some("Test Author".to_string()),
144 created_at: Utc::now(),
145 updated_at: Utc::now(),
146 phase: SpecPhase::Requirements,
147 status: SpecStatus::Draft,
148 },
149 inheritance: None,
150 };
151
152 let markdown = MarkdownParser::serialize(&spec).expect("Failed to serialize to Markdown");
153 let yaml = FormatConverter::markdown_to_yaml(&markdown)
154 .expect("Failed to convert Markdown to YAML");
155
156 assert!(yaml.contains("id: test-spec"));
157 assert!(yaml.contains("name: Test Spec"));
158 assert!(yaml.contains("version: '1.0'"));
159 }
160
161 #[test]
162 fn test_convert_yaml_to_markdown() {
163 let spec = Spec {
164 id: "test-spec".to_string(),
165 name: "Test Spec".to_string(),
166 version: "1.0".to_string(),
167 requirements: vec![],
168 design: None,
169 tasks: vec![],
170 metadata: SpecMetadata {
171 author: None,
172 created_at: Utc::now(),
173 updated_at: Utc::now(),
174 phase: SpecPhase::Requirements,
175 status: SpecStatus::Draft,
176 },
177 inheritance: None,
178 };
179
180 let yaml = YamlParser::serialize(&spec).expect("Failed to serialize to YAML");
181 let markdown =
182 FormatConverter::convert(&yaml, "yaml", "markdown").expect("Failed to convert");
183
184 assert!(markdown.contains("# Test Spec"));
185 }
186
187 #[test]
188 fn test_convert_markdown_to_yaml() {
189 let spec = Spec {
190 id: "test-spec".to_string(),
191 name: "Test Spec".to_string(),
192 version: "1.0".to_string(),
193 requirements: vec![],
194 design: None,
195 tasks: vec![],
196 metadata: SpecMetadata {
197 author: None,
198 created_at: Utc::now(),
199 updated_at: Utc::now(),
200 phase: SpecPhase::Requirements,
201 status: SpecStatus::Draft,
202 },
203 inheritance: None,
204 };
205
206 let markdown = MarkdownParser::serialize(&spec).expect("Failed to serialize to Markdown");
207 let yaml =
208 FormatConverter::convert(&markdown, "markdown", "yaml").expect("Failed to convert");
209
210 assert!(yaml.contains("id: test-spec"));
211 }
212
213 #[test]
214 fn test_convert_same_format_yaml() {
215 let spec = Spec {
216 id: "test-spec".to_string(),
217 name: "Test Spec".to_string(),
218 version: "1.0".to_string(),
219 requirements: vec![],
220 design: None,
221 tasks: vec![],
222 metadata: SpecMetadata {
223 author: None,
224 created_at: Utc::now(),
225 updated_at: Utc::now(),
226 phase: SpecPhase::Requirements,
227 status: SpecStatus::Draft,
228 },
229 inheritance: None,
230 };
231
232 let yaml = YamlParser::serialize(&spec).expect("Failed to serialize to YAML");
233 let normalized =
234 FormatConverter::convert(&yaml, "yaml", "yaml").expect("Failed to normalize YAML");
235
236 let parsed_original = YamlParser::parse(&yaml).expect("Failed to parse original");
238 let parsed_normalized = YamlParser::parse(&normalized).expect("Failed to parse normalized");
239
240 assert_eq!(parsed_original.id, parsed_normalized.id);
241 }
242
243 #[test]
244 fn test_convert_same_format_markdown() {
245 let spec = Spec {
246 id: "test-spec".to_string(),
247 name: "Test Spec".to_string(),
248 version: "1.0".to_string(),
249 requirements: vec![],
250 design: None,
251 tasks: vec![],
252 metadata: SpecMetadata {
253 author: None,
254 created_at: Utc::now(),
255 updated_at: Utc::now(),
256 phase: SpecPhase::Requirements,
257 status: SpecStatus::Draft,
258 },
259 inheritance: None,
260 };
261
262 let markdown = MarkdownParser::serialize(&spec).expect("Failed to serialize to Markdown");
263 let normalized = FormatConverter::convert(&markdown, "markdown", "markdown")
264 .expect("Failed to normalize Markdown");
265
266 let parsed_original = MarkdownParser::parse(&markdown).expect("Failed to parse original");
268 let parsed_normalized =
269 MarkdownParser::parse(&normalized).expect("Failed to parse normalized");
270
271 assert_eq!(parsed_original.id, parsed_normalized.id);
272 }
273
274 #[test]
275 fn test_convert_invalid_format() {
276 let result = FormatConverter::convert("content", "invalid", "yaml");
277 assert!(result.is_err());
278 }
279
280 #[test]
281 fn test_convert_case_insensitive() {
282 let spec = Spec {
283 id: "test-spec".to_string(),
284 name: "Test Spec".to_string(),
285 version: "1.0".to_string(),
286 requirements: vec![],
287 design: None,
288 tasks: vec![],
289 metadata: SpecMetadata {
290 author: None,
291 created_at: Utc::now(),
292 updated_at: Utc::now(),
293 phase: SpecPhase::Requirements,
294 status: SpecStatus::Draft,
295 },
296 inheritance: None,
297 };
298
299 let yaml = YamlParser::serialize(&spec).expect("Failed to serialize to YAML");
300
301 let result1 = FormatConverter::convert(&yaml, "YAML", "MARKDOWN");
303 assert!(result1.is_ok());
304
305 let result2 = FormatConverter::convert(&yaml, "YaMl", "MaRkDoWn");
307 assert!(result2.is_ok());
308 }
309}
310
311#[cfg(test)]
312mod property_tests {
313 use super::*;
314 use crate::models::*;
315 use chrono::Utc;
316 use proptest::prelude::*;
317
318 fn arb_spec() -> impl Strategy<Value = Spec> {
319 let valid_id = r"[a-z0-9][a-z0-9\-_]{0,20}";
320 let valid_name = r"[a-zA-Z0-9][a-zA-Z0-9 ]{0,29}";
321 let valid_version = r"[0-9]\.[0-9](\.[0-9])?";
322
323 (valid_id, valid_name, valid_version).prop_map(|(id, name, version)| {
324 let now = Utc::now();
325 Spec {
326 id,
327 name: name.trim().to_string(),
328 version,
329 requirements: vec![],
330 design: None,
331 tasks: vec![],
332 metadata: SpecMetadata {
333 author: Some("Test".to_string()),
334 created_at: now,
335 updated_at: now,
336 phase: SpecPhase::Requirements,
337 status: SpecStatus::Draft,
338 },
339 inheritance: None,
340 }
341 })
342 }
343
344 proptest! {
345 #[test]
350 fn prop_yaml_markdown_yaml_roundtrip(spec in arb_spec()) {
351 let yaml1 = YamlParser::serialize(&spec)
353 .expect("Failed to serialize to YAML");
354
355 let markdown = FormatConverter::yaml_to_markdown(&yaml1)
357 .expect("Failed to convert YAML to Markdown");
358
359 let yaml2 = FormatConverter::markdown_to_yaml(&markdown)
361 .expect("Failed to convert Markdown to YAML");
362
363 let parsed1 = YamlParser::parse(&yaml1)
365 .expect("Failed to parse original YAML");
366 let parsed2 = YamlParser::parse(&yaml2)
367 .expect("Failed to parse roundtrip YAML");
368
369 prop_assert_eq!(parsed1.id, parsed2.id, "ID should be preserved in YAML→MD→YAML");
371 prop_assert_eq!(parsed1.name, parsed2.name, "Name should be preserved in YAML→MD→YAML");
372 prop_assert_eq!(parsed1.version, parsed2.version, "Version should be preserved in YAML→MD→YAML");
373 prop_assert_eq!(parsed1.metadata.phase, parsed2.metadata.phase, "Phase should be preserved in YAML→MD→YAML");
374 prop_assert_eq!(parsed1.metadata.status, parsed2.metadata.status, "Status should be preserved in YAML→MD→YAML");
375 }
376
377 #[test]
382 fn prop_markdown_yaml_markdown_roundtrip(spec in arb_spec()) {
383 let markdown1 = MarkdownParser::serialize(&spec)
385 .expect("Failed to serialize to Markdown");
386
387 let yaml = FormatConverter::markdown_to_yaml(&markdown1)
389 .expect("Failed to convert Markdown to YAML");
390
391 let markdown2 = FormatConverter::yaml_to_markdown(&yaml)
393 .expect("Failed to convert YAML to Markdown");
394
395 let parsed1 = MarkdownParser::parse(&markdown1)
397 .expect("Failed to parse original Markdown");
398 let parsed2 = MarkdownParser::parse(&markdown2)
399 .expect("Failed to parse roundtrip Markdown");
400
401 prop_assert_eq!(parsed1.id, parsed2.id, "ID should be preserved in MD→YAML→MD");
403 prop_assert_eq!(parsed1.name, parsed2.name, "Name should be preserved in MD→YAML→MD");
404 prop_assert_eq!(parsed1.version, parsed2.version, "Version should be preserved in MD→YAML→MD");
405 prop_assert_eq!(parsed1.metadata.phase, parsed2.metadata.phase, "Phase should be preserved in MD→YAML→MD");
406 prop_assert_eq!(parsed1.metadata.status, parsed2.metadata.status, "Status should be preserved in MD→YAML→MD");
407 }
408
409 #[test]
414 fn prop_format_conversion_preserves_semantics(spec in arb_spec()) {
415 let yaml = YamlParser::serialize(&spec)
417 .expect("Failed to serialize to YAML");
418
419 let markdown = FormatConverter::yaml_to_markdown(&yaml)
421 .expect("Failed to convert to Markdown");
422
423 let from_yaml = YamlParser::parse(&yaml)
425 .expect("Failed to parse YAML");
426 let from_markdown = MarkdownParser::parse(&markdown)
427 .expect("Failed to parse Markdown");
428
429 prop_assert_eq!(from_yaml.id, from_markdown.id, "ID should match across formats");
431 prop_assert_eq!(from_yaml.name, from_markdown.name, "Name should match across formats");
432 prop_assert_eq!(from_yaml.version, from_markdown.version, "Version should match across formats");
433 prop_assert_eq!(from_yaml.metadata.phase, from_markdown.metadata.phase, "Phase should match across formats");
434 prop_assert_eq!(from_yaml.metadata.status, from_markdown.metadata.status, "Status should match across formats");
435 }
436 }
437}