1use crate::{DrivenConfig, DrivenError, Result};
30use std::path::Path;
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum LegacyFormat {
35 Json,
37 Yaml,
39 Toml,
41}
42
43impl LegacyFormat {
44 pub fn from_extension(path: &Path) -> Option<Self> {
46 path.extension()
47 .and_then(|ext| ext.to_str())
48 .and_then(|ext| match ext.to_lowercase().as_str() {
49 "json" => Some(LegacyFormat::Json),
50 "yaml" | "yml" => Some(LegacyFormat::Yaml),
51 "toml" => Some(LegacyFormat::Toml),
52 _ => None,
53 })
54 }
55
56 pub fn extension(&self) -> &'static str {
58 match self {
59 LegacyFormat::Json => "json",
60 LegacyFormat::Yaml => "yaml",
61 LegacyFormat::Toml => "toml",
62 }
63 }
64
65 pub fn mime_type(&self) -> &'static str {
67 match self {
68 LegacyFormat::Json => "application/json",
69 LegacyFormat::Yaml => "application/x-yaml",
70 LegacyFormat::Toml => "application/toml",
71 }
72 }
73}
74
75pub struct LegacyConverter;
79
80impl LegacyConverter {
81 pub fn from_json(content: &str) -> Result<DrivenConfig> {
85 serde_json::from_str(content)
86 .map_err(|e| DrivenError::Parse(format!("JSON parse error: {}", e)))
87 }
88
89 pub fn to_json(config: &DrivenConfig) -> Result<String> {
91 serde_json::to_string_pretty(config)
92 .map_err(|e| DrivenError::Format(format!("JSON serialization error: {}", e)))
93 }
94
95 pub fn to_json_compact(config: &DrivenConfig) -> Result<String> {
97 serde_json::to_string(config)
98 .map_err(|e| DrivenError::Format(format!("JSON serialization error: {}", e)))
99 }
100
101 pub fn from_yaml(content: &str) -> Result<DrivenConfig> {
105 serde_yaml::from_str(content)
106 .map_err(|e| DrivenError::Parse(format!("YAML parse error: {}", e)))
107 }
108
109 pub fn to_yaml(config: &DrivenConfig) -> Result<String> {
111 serde_yaml::to_string(config)
112 .map_err(|e| DrivenError::Format(format!("YAML serialization error: {}", e)))
113 }
114
115 pub fn from_toml(content: &str) -> Result<DrivenConfig> {
119 toml::from_str(content).map_err(|e| DrivenError::Parse(format!("TOML parse error: {}", e)))
120 }
121
122 pub fn to_toml(config: &DrivenConfig) -> Result<String> {
124 toml::to_string_pretty(config)
125 .map_err(|e| DrivenError::Format(format!("TOML serialization error: {}", e)))
126 }
127
128 pub fn from_auto(content: &str, path: &Path) -> Result<DrivenConfig> {
134 let format = LegacyFormat::from_extension(path).ok_or_else(|| {
135 DrivenError::UnsupportedFormat(format!(
136 "Cannot detect format from path: {}",
137 path.display()
138 ))
139 })?;
140
141 Self::from_format(content, format)
142 }
143
144 pub fn from_format(content: &str, format: LegacyFormat) -> Result<DrivenConfig> {
146 match format {
147 LegacyFormat::Json => Self::from_json(content),
148 LegacyFormat::Yaml => Self::from_yaml(content),
149 LegacyFormat::Toml => Self::from_toml(content),
150 }
151 }
152
153 pub fn to_format(config: &DrivenConfig, format: LegacyFormat) -> Result<String> {
155 match format {
156 LegacyFormat::Json => Self::to_json(config),
157 LegacyFormat::Yaml => Self::to_yaml(config),
158 LegacyFormat::Toml => Self::to_toml(config),
159 }
160 }
161
162 pub fn load_file(path: &Path) -> Result<DrivenConfig> {
166 let content = std::fs::read_to_string(path)?;
167 Self::from_auto(&content, path)
168 }
169
170 pub fn save_file(config: &DrivenConfig, path: &Path) -> Result<()> {
174 let format = LegacyFormat::from_extension(path).ok_or_else(|| {
175 DrivenError::UnsupportedFormat(format!(
176 "Cannot detect format from path: {}",
177 path.display()
178 ))
179 })?;
180
181 let content = Self::to_format(config, format)?;
182 std::fs::write(path, content)?;
183 Ok(())
184 }
185
186 pub fn convert(
190 content: &str,
191 from_format: LegacyFormat,
192 to_format: LegacyFormat,
193 ) -> Result<String> {
194 let config = Self::from_format(content, from_format)?;
195 Self::to_format(&config, to_format)
196 }
197
198 pub fn convert_file(input_path: &Path, output_path: &Path) -> Result<()> {
200 let config = Self::load_file(input_path)?;
201 Self::save_file(&config, output_path)
202 }
203}
204
205pub trait LegacySerializable: Sized {
207 fn from_legacy_json(content: &str) -> Result<Self>;
209
210 fn to_legacy_json(&self) -> Result<String>;
212
213 fn from_legacy_yaml(content: &str) -> Result<Self>;
215
216 fn to_legacy_yaml(&self) -> Result<String>;
218
219 fn from_legacy_toml(content: &str) -> Result<Self>;
221
222 fn to_legacy_toml(&self) -> Result<String>;
224}
225
226impl LegacySerializable for DrivenConfig {
227 fn from_legacy_json(content: &str) -> Result<Self> {
228 LegacyConverter::from_json(content)
229 }
230
231 fn to_legacy_json(&self) -> Result<String> {
232 LegacyConverter::to_json(self)
233 }
234
235 fn from_legacy_yaml(content: &str) -> Result<Self> {
236 LegacyConverter::from_yaml(content)
237 }
238
239 fn to_legacy_yaml(&self) -> Result<String> {
240 LegacyConverter::to_yaml(self)
241 }
242
243 fn from_legacy_toml(content: &str) -> Result<Self> {
244 LegacyConverter::from_toml(content)
245 }
246
247 fn to_legacy_toml(&self) -> Result<String> {
248 LegacyConverter::to_toml(self)
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255 use crate::Editor;
256
257 #[test]
258 fn test_json_roundtrip() {
259 let config = DrivenConfig::default();
260 let json = LegacyConverter::to_json(&config).unwrap();
261 let loaded = LegacyConverter::from_json(&json).unwrap();
262
263 assert_eq!(config.version, loaded.version);
264 assert_eq!(config.default_editor, loaded.default_editor);
265 assert_eq!(config.editors.cursor, loaded.editors.cursor);
266 }
267
268 #[test]
269 fn test_yaml_roundtrip() {
270 let config = DrivenConfig::default();
271 let yaml = LegacyConverter::to_yaml(&config).unwrap();
272 let loaded = LegacyConverter::from_yaml(&yaml).unwrap();
273
274 assert_eq!(config.version, loaded.version);
275 assert_eq!(config.default_editor, loaded.default_editor);
276 assert_eq!(config.editors.cursor, loaded.editors.cursor);
277 }
278
279 #[test]
280 fn test_toml_roundtrip() {
281 let config = DrivenConfig::default();
282 let toml_str = LegacyConverter::to_toml(&config).unwrap();
283 let loaded = LegacyConverter::from_toml(&toml_str).unwrap();
284
285 assert_eq!(config.version, loaded.version);
286 assert_eq!(config.default_editor, loaded.default_editor);
287 assert_eq!(config.editors.cursor, loaded.editors.cursor);
288 }
289
290 #[test]
291 fn test_format_detection() {
292 assert_eq!(
293 LegacyFormat::from_extension(Path::new("config.json")),
294 Some(LegacyFormat::Json)
295 );
296 assert_eq!(
297 LegacyFormat::from_extension(Path::new("config.yaml")),
298 Some(LegacyFormat::Yaml)
299 );
300 assert_eq!(
301 LegacyFormat::from_extension(Path::new("config.yml")),
302 Some(LegacyFormat::Yaml)
303 );
304 assert_eq!(
305 LegacyFormat::from_extension(Path::new("config.toml")),
306 Some(LegacyFormat::Toml)
307 );
308 assert_eq!(LegacyFormat::from_extension(Path::new("config.txt")), None);
309 }
310
311 #[test]
312 fn test_format_conversion() {
313 let config = DrivenConfig::default();
314
315 let json = LegacyConverter::to_json(&config).unwrap();
317 let yaml = LegacyConverter::convert(&json, LegacyFormat::Json, LegacyFormat::Yaml).unwrap();
318 let loaded = LegacyConverter::from_yaml(&yaml).unwrap();
319 assert_eq!(config.version, loaded.version);
320
321 let toml_str =
323 LegacyConverter::convert(&yaml, LegacyFormat::Yaml, LegacyFormat::Toml).unwrap();
324 let loaded = LegacyConverter::from_toml(&toml_str).unwrap();
325 assert_eq!(config.version, loaded.version);
326
327 let json2 =
329 LegacyConverter::convert(&toml_str, LegacyFormat::Toml, LegacyFormat::Json).unwrap();
330 let loaded = LegacyConverter::from_json(&json2).unwrap();
331 assert_eq!(config.version, loaded.version);
332 }
333
334 #[test]
335 fn test_legacy_serializable_trait() {
336 let config = DrivenConfig::default();
337
338 let json = config.to_legacy_json().unwrap();
340 let loaded = DrivenConfig::from_legacy_json(&json).unwrap();
341 assert_eq!(config.version, loaded.version);
342
343 let yaml = config.to_legacy_yaml().unwrap();
345 let loaded = DrivenConfig::from_legacy_yaml(&yaml).unwrap();
346 assert_eq!(config.version, loaded.version);
347
348 let toml_str = config.to_legacy_toml().unwrap();
350 let loaded = DrivenConfig::from_legacy_toml(&toml_str).unwrap();
351 assert_eq!(config.version, loaded.version);
352 }
353
354 #[test]
355 fn test_json_with_all_fields() {
356 let json = r#"{
357 "version": "2.0",
358 "default_editor": "copilot",
359 "editors": {
360 "cursor": false,
361 "copilot": true,
362 "windsurf": true,
363 "claude": false,
364 "aider": true,
365 "cline": false
366 },
367 "templates": {
368 "personas": ["developer", "reviewer"],
369 "project": "rust",
370 "standards": ["clean-code"],
371 "workflow": "agile"
372 },
373 "sync": {
374 "watch": false,
375 "auto_convert": true,
376 "source_of_truth": "custom/path.drv"
377 },
378 "context": {
379 "include": ["src/**", "lib/**"],
380 "exclude": ["target/**"],
381 "index_path": "custom/index.drv"
382 }
383 }"#;
384
385 let config = LegacyConverter::from_json(json).unwrap();
386 assert_eq!(config.version, "2.0");
387 assert_eq!(config.default_editor, Editor::Copilot);
388 assert!(!config.editors.cursor);
389 assert!(config.editors.copilot);
390 assert!(config.editors.windsurf);
391 assert!(!config.sync.watch);
392 assert_eq!(config.sync.source_of_truth, "custom/path.drv");
393 }
394}
395
396#[cfg(test)]
397mod prop_tests {
398 use super::*;
399 use crate::{ContextConfig, Editor, EditorConfig, SyncConfig, TemplateConfig};
400 use proptest::prelude::*;
401
402 fn arb_editor() -> impl Strategy<Value = Editor> {
404 prop_oneof![
405 Just(Editor::Cursor),
406 Just(Editor::Copilot),
407 Just(Editor::Windsurf),
408 Just(Editor::Claude),
409 Just(Editor::Aider),
410 Just(Editor::Cline),
411 ]
412 }
413
414 fn arb_editor_config() -> impl Strategy<Value = EditorConfig> {
416 (
417 any::<bool>(),
418 any::<bool>(),
419 any::<bool>(),
420 any::<bool>(),
421 any::<bool>(),
422 any::<bool>(),
423 )
424 .prop_map(
425 |(cursor, copilot, windsurf, claude, aider, cline)| EditorConfig {
426 cursor,
427 copilot,
428 windsurf,
429 claude,
430 aider,
431 cline,
432 },
433 )
434 }
435
436 fn arb_sync_config() -> impl Strategy<Value = SyncConfig> {
438 (any::<bool>(), any::<bool>(), "[a-zA-Z0-9_./]{1,50}").prop_map(
439 |(watch, auto_convert, source)| SyncConfig {
440 watch,
441 auto_convert,
442 source_of_truth: source,
443 },
444 )
445 }
446
447 fn arb_template_config() -> impl Strategy<Value = TemplateConfig> {
449 (
450 prop::collection::vec("[a-zA-Z0-9_-]{1,20}", 0..5),
451 prop::option::of("[a-zA-Z0-9_-]{1,20}"),
452 prop::collection::vec("[a-zA-Z0-9_-]{1,20}", 0..5),
453 prop::option::of("[a-zA-Z0-9_-]{1,20}"),
454 )
455 .prop_map(|(personas, project, standards, workflow)| TemplateConfig {
456 personas,
457 project,
458 standards,
459 workflow,
460 })
461 }
462
463 fn arb_context_config() -> impl Strategy<Value = ContextConfig> {
465 (
466 prop::collection::vec("[a-zA-Z0-9_*/.]{1,30}", 0..5),
467 prop::collection::vec("[a-zA-Z0-9_*/.]{1,30}", 0..5),
468 "[a-zA-Z0-9_./]{1,50}",
469 )
470 .prop_map(|(include, exclude, index_path)| ContextConfig {
471 include,
472 exclude,
473 index_path,
474 })
475 }
476
477 fn arb_driven_config() -> impl Strategy<Value = DrivenConfig> {
479 (
480 "[0-9]+\\.[0-9]+",
481 arb_editor(),
482 arb_editor_config(),
483 arb_sync_config(),
484 arb_template_config(),
485 arb_context_config(),
486 )
487 .prop_map(
488 |(version, default_editor, editors, sync, templates, context)| DrivenConfig {
489 version,
490 default_editor,
491 editors,
492 sync,
493 templates,
494 context,
495 },
496 )
497 }
498
499 proptest! {
500 #[test]
505 fn prop_json_roundtrip(config in arb_driven_config()) {
506 let json = LegacyConverter::to_json(&config).expect("JSON serialization should succeed");
507 let loaded = LegacyConverter::from_json(&json).expect("JSON deserialization should succeed");
508
509 prop_assert_eq!(config.version, loaded.version);
511 prop_assert_eq!(config.default_editor, loaded.default_editor);
512 prop_assert_eq!(config.editors.cursor, loaded.editors.cursor);
513 prop_assert_eq!(config.editors.copilot, loaded.editors.copilot);
514 prop_assert_eq!(config.editors.windsurf, loaded.editors.windsurf);
515 prop_assert_eq!(config.editors.claude, loaded.editors.claude);
516 prop_assert_eq!(config.editors.aider, loaded.editors.aider);
517 prop_assert_eq!(config.editors.cline, loaded.editors.cline);
518 prop_assert_eq!(config.sync.watch, loaded.sync.watch);
519 prop_assert_eq!(config.sync.auto_convert, loaded.sync.auto_convert);
520 prop_assert_eq!(config.sync.source_of_truth, loaded.sync.source_of_truth);
521 prop_assert_eq!(config.templates.personas, loaded.templates.personas);
522 prop_assert_eq!(config.templates.project, loaded.templates.project);
523 prop_assert_eq!(config.templates.standards, loaded.templates.standards);
524 prop_assert_eq!(config.templates.workflow, loaded.templates.workflow);
525 prop_assert_eq!(config.context.include, loaded.context.include);
526 prop_assert_eq!(config.context.exclude, loaded.context.exclude);
527 prop_assert_eq!(config.context.index_path, loaded.context.index_path);
528 }
529
530 #[test]
535 fn prop_yaml_roundtrip(config in arb_driven_config()) {
536 let yaml = LegacyConverter::to_yaml(&config).expect("YAML serialization should succeed");
537 let loaded = LegacyConverter::from_yaml(&yaml).expect("YAML deserialization should succeed");
538
539 prop_assert_eq!(config.version, loaded.version);
541 prop_assert_eq!(config.default_editor, loaded.default_editor);
542 prop_assert_eq!(config.editors.cursor, loaded.editors.cursor);
543 prop_assert_eq!(config.editors.copilot, loaded.editors.copilot);
544 prop_assert_eq!(config.editors.windsurf, loaded.editors.windsurf);
545 prop_assert_eq!(config.editors.claude, loaded.editors.claude);
546 prop_assert_eq!(config.editors.aider, loaded.editors.aider);
547 prop_assert_eq!(config.editors.cline, loaded.editors.cline);
548 prop_assert_eq!(config.sync.watch, loaded.sync.watch);
549 prop_assert_eq!(config.sync.auto_convert, loaded.sync.auto_convert);
550 prop_assert_eq!(config.sync.source_of_truth, loaded.sync.source_of_truth);
551 prop_assert_eq!(config.templates.personas, loaded.templates.personas);
552 prop_assert_eq!(config.templates.project, loaded.templates.project);
553 prop_assert_eq!(config.templates.standards, loaded.templates.standards);
554 prop_assert_eq!(config.templates.workflow, loaded.templates.workflow);
555 prop_assert_eq!(config.context.include, loaded.context.include);
556 prop_assert_eq!(config.context.exclude, loaded.context.exclude);
557 prop_assert_eq!(config.context.index_path, loaded.context.index_path);
558 }
559
560 #[test]
565 fn prop_toml_roundtrip(config in arb_driven_config()) {
566 let toml_str = LegacyConverter::to_toml(&config).expect("TOML serialization should succeed");
567 let loaded = LegacyConverter::from_toml(&toml_str).expect("TOML deserialization should succeed");
568
569 prop_assert_eq!(config.version, loaded.version);
571 prop_assert_eq!(config.default_editor, loaded.default_editor);
572 prop_assert_eq!(config.editors.cursor, loaded.editors.cursor);
573 prop_assert_eq!(config.editors.copilot, loaded.editors.copilot);
574 prop_assert_eq!(config.editors.windsurf, loaded.editors.windsurf);
575 prop_assert_eq!(config.editors.claude, loaded.editors.claude);
576 prop_assert_eq!(config.editors.aider, loaded.editors.aider);
577 prop_assert_eq!(config.editors.cline, loaded.editors.cline);
578 prop_assert_eq!(config.sync.watch, loaded.sync.watch);
579 prop_assert_eq!(config.sync.auto_convert, loaded.sync.auto_convert);
580 prop_assert_eq!(config.sync.source_of_truth, loaded.sync.source_of_truth);
581 prop_assert_eq!(config.templates.personas, loaded.templates.personas);
582 prop_assert_eq!(config.templates.project, loaded.templates.project);
583 prop_assert_eq!(config.templates.standards, loaded.templates.standards);
584 prop_assert_eq!(config.templates.workflow, loaded.templates.workflow);
585 prop_assert_eq!(config.context.include, loaded.context.include);
586 prop_assert_eq!(config.context.exclude, loaded.context.exclude);
587 prop_assert_eq!(config.context.index_path, loaded.context.index_path);
588 }
589
590 #[test]
595 fn prop_cross_format_conversion(config in arb_driven_config()) {
596 let json1 = LegacyConverter::to_json(&config).expect("JSON serialization should succeed");
598 let yaml = LegacyConverter::convert(&json1, LegacyFormat::Json, LegacyFormat::Yaml)
599 .expect("JSON to YAML conversion should succeed");
600 let toml_str = LegacyConverter::convert(&yaml, LegacyFormat::Yaml, LegacyFormat::Toml)
601 .expect("YAML to TOML conversion should succeed");
602 let json2 = LegacyConverter::convert(&toml_str, LegacyFormat::Toml, LegacyFormat::Json)
603 .expect("TOML to JSON conversion should succeed");
604
605 let loaded = LegacyConverter::from_json(&json2).expect("Final JSON deserialization should succeed");
606
607 prop_assert_eq!(config.version, loaded.version);
609 prop_assert_eq!(config.default_editor, loaded.default_editor);
610 prop_assert_eq!(config.editors.cursor, loaded.editors.cursor);
611 prop_assert_eq!(config.editors.copilot, loaded.editors.copilot);
612 prop_assert_eq!(config.editors.windsurf, loaded.editors.windsurf);
613 prop_assert_eq!(config.editors.claude, loaded.editors.claude);
614 prop_assert_eq!(config.editors.aider, loaded.editors.aider);
615 prop_assert_eq!(config.editors.cline, loaded.editors.cline);
616 prop_assert_eq!(config.sync.watch, loaded.sync.watch);
617 prop_assert_eq!(config.sync.auto_convert, loaded.sync.auto_convert);
618 prop_assert_eq!(config.sync.source_of_truth, loaded.sync.source_of_truth);
619 }
620 }
621}