1pub mod legacy;
32mod markdown;
33
34use crate::{DrivenConfig, DrivenError, EditorConfig, Result};
35use serializer::{DxDocument, DxLlmValue, DxSection, document_to_llm, llm_to_document};
36
37pub use serializer as dx_serializer;
39
40pub use markdown::{DxDocumentable, DxMarkdownConfig, DxMarkdownFormat, rules_to_dx_markdown};
42
43pub use legacy::{LegacyConverter, LegacyFormat, LegacySerializable};
45
46pub trait DxSerializable: Sized {
48 fn to_dx_llm(&self) -> Result<String>;
50
51 fn from_dx_llm(content: &str) -> Result<Self>;
53
54 fn to_dx_machine(&self) -> Result<Vec<u8>>;
56
57 fn from_dx_machine(data: &[u8]) -> Result<Self>;
59}
60
61impl DxSerializable for DrivenConfig {
62 fn to_dx_llm(&self) -> Result<String> {
63 let mut doc = DxDocument::new();
64
65 doc.context.insert(
67 "nm".to_string(),
68 DxLlmValue::Str("driven-config".to_string()),
69 );
70 doc.context
71 .insert("v".to_string(), DxLlmValue::Str(self.version.clone()));
72 doc.context.insert(
73 "editor".to_string(),
74 DxLlmValue::Str(self.default_editor.to_string()),
75 );
76
77 let mut editors_section = DxSection::new(vec!["name".to_string(), "enabled".to_string()]);
79 editors_section
80 .add_row(vec![
81 DxLlmValue::Str("cursor".to_string()),
82 DxLlmValue::Bool(self.editors.cursor),
83 ])
84 .ok();
85 editors_section
86 .add_row(vec![
87 DxLlmValue::Str("copilot".to_string()),
88 DxLlmValue::Bool(self.editors.copilot),
89 ])
90 .ok();
91 editors_section
92 .add_row(vec![
93 DxLlmValue::Str("windsurf".to_string()),
94 DxLlmValue::Bool(self.editors.windsurf),
95 ])
96 .ok();
97 editors_section
98 .add_row(vec![
99 DxLlmValue::Str("claude".to_string()),
100 DxLlmValue::Bool(self.editors.claude),
101 ])
102 .ok();
103 editors_section
104 .add_row(vec![
105 DxLlmValue::Str("aider".to_string()),
106 DxLlmValue::Bool(self.editors.aider),
107 ])
108 .ok();
109 editors_section
110 .add_row(vec![
111 DxLlmValue::Str("cline".to_string()),
112 DxLlmValue::Bool(self.editors.cline),
113 ])
114 .ok();
115 doc.sections.insert('e', editors_section);
116
117 let mut sync_section = DxSection::new(vec!["key".to_string(), "value".to_string()]);
119 sync_section
120 .add_row(vec![
121 DxLlmValue::Str("watch".to_string()),
122 DxLlmValue::Bool(self.sync.watch),
123 ])
124 .ok();
125 sync_section
126 .add_row(vec![
127 DxLlmValue::Str("auto_convert".to_string()),
128 DxLlmValue::Bool(self.sync.auto_convert),
129 ])
130 .ok();
131 sync_section
132 .add_row(vec![
133 DxLlmValue::Str("source".to_string()),
134 DxLlmValue::Str(self.sync.source_of_truth.clone()),
135 ])
136 .ok();
137 doc.sections.insert('s', sync_section);
138
139 Ok(document_to_llm(&doc))
140 }
141
142 fn from_dx_llm(content: &str) -> Result<Self> {
143 let doc = llm_to_document(content)
144 .map_err(|e| DrivenError::Parse(format!("DX LLM parse error: {}", e)))?;
145
146 let mut config = DrivenConfig::default();
147
148 if let Some(v) = doc.context.get("v").or_else(|| doc.context.get("vr")) {
151 config.version = match v {
152 DxLlmValue::Str(s) => s.clone(),
153 DxLlmValue::Num(n) => {
154 if n.fract() == 0.0 {
156 format!("{}.0", *n as i64)
157 } else {
158 format!("{}", n)
159 }
160 }
161 _ => config.version.clone(),
162 };
163 }
164
165 if let Some(v) = doc.context.get("editor").or_else(|| doc.context.get("ed")) {
167 if let Some(s) = v.as_str() {
168 config.default_editor = parse_editor(s)?;
169 }
170 }
171
172 if let Some(section) = doc.sections.get(&'e') {
174 for row in §ion.rows {
175 if row.len() >= 2 {
176 if let (Some(name), Some(enabled)) = (row[0].as_str(), row[1].as_bool()) {
178 match name {
179 "cursor" => config.editors.cursor = enabled,
180 "copilot" => config.editors.copilot = enabled,
181 "windsurf" => config.editors.windsurf = enabled,
182 "claude" => config.editors.claude = enabled,
183 "aider" => config.editors.aider = enabled,
184 "cline" => config.editors.cline = enabled,
185 _ => {}
186 }
187 }
188 }
189 }
190 }
191
192 if let Some(section) = doc.sections.get(&'s') {
194 for row in §ion.rows {
195 if row.len() >= 2 {
196 if let Some(key) = row[0].as_str() {
198 match key {
199 "watch" => {
200 if let Some(v) = row[1].as_bool() {
201 config.sync.watch = v;
202 }
203 }
204 "auto_convert" => {
205 if let Some(v) = row[1].as_bool() {
206 config.sync.auto_convert = v;
207 }
208 }
209 "source" => {
210 config.sync.source_of_truth = match &row[1] {
212 DxLlmValue::Str(s) => s.clone(),
213 DxLlmValue::Num(n) => {
214 if n.fract() == 0.0 {
215 format!("{}", *n as i64)
216 } else {
217 format!("{}", n)
218 }
219 }
220 _ => config.sync.source_of_truth.clone(),
221 };
222 }
223 _ => {}
224 }
225 }
226 }
227 }
228 }
229
230 Ok(config)
231 }
232
233 fn to_dx_machine(&self) -> Result<Vec<u8>> {
234 let version_bytes = self.version.as_bytes();
246 let source_bytes = self.sync.source_of_truth.as_bytes();
247
248 let total_len = 12 + version_bytes.len() + source_bytes.len();
249 let mut buffer = Vec::with_capacity(total_len);
250
251 buffer.extend_from_slice(b"DRV1");
253
254 let editor_flags: u8 = (self.editors.cursor as u8)
256 | ((self.editors.copilot as u8) << 1)
257 | ((self.editors.windsurf as u8) << 2)
258 | ((self.editors.claude as u8) << 3)
259 | ((self.editors.aider as u8) << 4)
260 | ((self.editors.cline as u8) << 5);
261 buffer.push(editor_flags);
262
263 let sync_flags: u8 = (self.sync.watch as u8) | ((self.sync.auto_convert as u8) << 1);
265 buffer.push(sync_flags);
266
267 buffer.push(self.default_editor as u8);
269
270 buffer.push(0);
272
273 buffer.extend_from_slice(&(version_bytes.len() as u16).to_le_bytes());
275 buffer.extend_from_slice(&(source_bytes.len() as u16).to_le_bytes());
276
277 buffer.extend_from_slice(version_bytes);
279 buffer.extend_from_slice(source_bytes);
280
281 Ok(buffer)
282 }
283
284 fn from_dx_machine(data: &[u8]) -> Result<Self> {
285 if data.len() < 12 {
286 return Err(DrivenError::InvalidBinary(
287 "Data too short for DrivenConfig".to_string(),
288 ));
289 }
290
291 if &data[0..4] != b"DRV1" {
293 return Err(DrivenError::InvalidBinary(
294 "Invalid magic bytes".to_string(),
295 ));
296 }
297
298 let editor_flags = data[4];
299 let sync_flags = data[5];
300 let default_editor_byte = data[6];
301
302 let version_len = u16::from_le_bytes([data[8], data[9]]) as usize;
303 let source_len = u16::from_le_bytes([data[10], data[11]]) as usize;
304
305 if data.len() < 12 + version_len + source_len {
306 return Err(DrivenError::InvalidBinary(
307 "Data too short for strings".to_string(),
308 ));
309 }
310
311 let version = std::str::from_utf8(&data[12..12 + version_len])
312 .map_err(|e| DrivenError::InvalidBinary(format!("Invalid UTF-8 in version: {}", e)))?
313 .to_string();
314
315 let source_of_truth =
316 std::str::from_utf8(&data[12 + version_len..12 + version_len + source_len])
317 .map_err(|e| DrivenError::InvalidBinary(format!("Invalid UTF-8 in source: {}", e)))?
318 .to_string();
319
320 Ok(DrivenConfig {
321 version,
322 default_editor: editor_from_byte(default_editor_byte)?,
323 editors: EditorConfig {
324 cursor: (editor_flags & 1) != 0,
325 copilot: (editor_flags & 2) != 0,
326 windsurf: (editor_flags & 4) != 0,
327 claude: (editor_flags & 8) != 0,
328 aider: (editor_flags & 16) != 0,
329 cline: (editor_flags & 32) != 0,
330 },
331 sync: crate::SyncConfig {
332 watch: (sync_flags & 1) != 0,
333 auto_convert: (sync_flags & 2) != 0,
334 source_of_truth,
335 },
336 templates: crate::TemplateConfig::default(),
337 context: crate::ContextConfig::default(),
338 })
339 }
340}
341
342#[allow(dead_code)]
344fn read_slot_string(_data: &[u8], _slot_offset: usize) -> Result<String> {
345 Err(DrivenError::InvalidBinary("Not implemented".to_string()))
347 }
387
388fn parse_editor(s: &str) -> Result<crate::Editor> {
390 match s.to_lowercase().as_str() {
391 "cursor" => Ok(crate::Editor::Cursor),
392 "copilot" | "github copilot" => Ok(crate::Editor::Copilot),
393 "windsurf" => Ok(crate::Editor::Windsurf),
394 "claude" | "claude code" => Ok(crate::Editor::Claude),
395 "aider" => Ok(crate::Editor::Aider),
396 "cline" => Ok(crate::Editor::Cline),
397 _ => Err(DrivenError::Parse(format!("Unknown editor: {}", s))),
398 }
399}
400
401fn editor_from_byte(byte: u8) -> Result<crate::Editor> {
403 match byte {
404 0 => Ok(crate::Editor::Cursor),
405 1 => Ok(crate::Editor::Copilot),
406 2 => Ok(crate::Editor::Windsurf),
407 3 => Ok(crate::Editor::Claude),
408 4 => Ok(crate::Editor::Aider),
409 5 => Ok(crate::Editor::Cline),
410 _ => Err(DrivenError::InvalidBinary(format!(
411 "Unknown editor byte: {}",
412 byte
413 ))),
414 }
415}
416
417#[cfg(test)]
418mod tests {
419 use super::*;
420
421 #[test]
422 fn test_driven_config_dx_llm_roundtrip() {
423 let config = DrivenConfig::default();
424 let llm = config.to_dx_llm().unwrap();
425 println!("Serialized LLM:\n{}", llm);
426 let loaded = DrivenConfig::from_dx_llm(&llm).unwrap();
427
428 assert_eq!(config.version, loaded.version);
429 assert_eq!(config.editors.cursor, loaded.editors.cursor);
430 assert_eq!(config.editors.copilot, loaded.editors.copilot);
431 assert_eq!(config.sync.watch, loaded.sync.watch);
432 }
433
434 #[test]
435 fn test_driven_config_dx_llm_roundtrip_numeric_version() {
436 let mut config = DrivenConfig::default();
437 config.version = "0.0".to_string();
438 let llm = config.to_dx_llm().unwrap();
439 println!("Serialized LLM:\n{}", llm);
440 let loaded = DrivenConfig::from_dx_llm(&llm).unwrap();
441 println!("Loaded version: {}", loaded.version);
442
443 assert_eq!(config.version, loaded.version);
444 }
445
446 #[test]
447 fn test_driven_config_dx_machine_roundtrip() {
448 let config = DrivenConfig::default();
449 let binary = config.to_dx_machine().unwrap();
450 let loaded = DrivenConfig::from_dx_machine(&binary).unwrap();
451
452 assert_eq!(config.version, loaded.version);
453 assert_eq!(config.editors.cursor, loaded.editors.cursor);
454 assert_eq!(config.editors.copilot, loaded.editors.copilot);
455 assert_eq!(config.sync.watch, loaded.sync.watch);
456 }
457}
458
459#[cfg(test)]
460mod prop_tests {
461 use super::*;
462 use proptest::prelude::*;
463
464 fn arb_editor() -> impl Strategy<Value = crate::Editor> {
466 prop_oneof![
467 Just(crate::Editor::Cursor),
468 Just(crate::Editor::Copilot),
469 Just(crate::Editor::Windsurf),
470 Just(crate::Editor::Claude),
471 Just(crate::Editor::Aider),
472 Just(crate::Editor::Cline),
473 ]
474 }
475
476 fn arb_editor_config() -> impl Strategy<Value = EditorConfig> {
478 (
479 any::<bool>(),
480 any::<bool>(),
481 any::<bool>(),
482 any::<bool>(),
483 any::<bool>(),
484 any::<bool>(),
485 )
486 .prop_map(
487 |(cursor, copilot, windsurf, claude, aider, cline)| EditorConfig {
488 cursor,
489 copilot,
490 windsurf,
491 claude,
492 aider,
493 cline,
494 },
495 )
496 }
497
498 fn arb_sync_config() -> impl Strategy<Value = crate::SyncConfig> {
500 (any::<bool>(), any::<bool>(), "[a-zA-Z0-9_./]{1,50}").prop_map(
501 |(watch, auto_convert, source)| crate::SyncConfig {
502 watch,
503 auto_convert,
504 source_of_truth: source,
505 },
506 )
507 }
508
509 fn arb_driven_config() -> impl Strategy<Value = DrivenConfig> {
511 (
512 "[0-9]+\\.[0-9]+\\.[0-9]+",
514 arb_editor(),
515 arb_editor_config(),
516 arb_sync_config(),
517 )
518 .prop_map(|(version, default_editor, editors, sync)| DrivenConfig {
519 version: format!("v{}", version), default_editor,
521 editors,
522 sync,
523 templates: crate::TemplateConfig::default(),
524 context: crate::ContextConfig::default(),
525 })
526 }
527
528 proptest! {
529 #[test]
534 fn prop_dx_llm_roundtrip(config in arb_driven_config()) {
535 let llm = config.to_dx_llm().expect("Serialization should succeed");
536 let loaded = DrivenConfig::from_dx_llm(&llm).expect("Deserialization should succeed");
537
538 prop_assert_eq!(config.version, loaded.version);
540 prop_assert_eq!(config.default_editor, loaded.default_editor);
541 prop_assert_eq!(config.editors.cursor, loaded.editors.cursor);
542 prop_assert_eq!(config.editors.copilot, loaded.editors.copilot);
543 prop_assert_eq!(config.editors.windsurf, loaded.editors.windsurf);
544 prop_assert_eq!(config.editors.claude, loaded.editors.claude);
545 prop_assert_eq!(config.editors.aider, loaded.editors.aider);
546 prop_assert_eq!(config.editors.cline, loaded.editors.cline);
547 prop_assert_eq!(config.sync.watch, loaded.sync.watch);
548 prop_assert_eq!(config.sync.auto_convert, loaded.sync.auto_convert);
549 prop_assert_eq!(config.sync.source_of_truth, loaded.sync.source_of_truth);
550 }
551
552 #[test]
557 fn prop_dx_machine_roundtrip(config in arb_driven_config()) {
558 let binary = config.to_dx_machine().expect("Serialization should succeed");
559 let loaded = DrivenConfig::from_dx_machine(&binary).expect("Deserialization should succeed");
560
561 prop_assert_eq!(config.version, loaded.version);
563 prop_assert_eq!(config.default_editor, loaded.default_editor);
564 prop_assert_eq!(config.editors.cursor, loaded.editors.cursor);
565 prop_assert_eq!(config.editors.copilot, loaded.editors.copilot);
566 prop_assert_eq!(config.editors.windsurf, loaded.editors.windsurf);
567 prop_assert_eq!(config.editors.claude, loaded.editors.claude);
568 prop_assert_eq!(config.editors.aider, loaded.editors.aider);
569 prop_assert_eq!(config.editors.cline, loaded.editors.cline);
570 prop_assert_eq!(config.sync.watch, loaded.sync.watch);
571 prop_assert_eq!(config.sync.auto_convert, loaded.sync.auto_convert);
572 prop_assert_eq!(config.sync.source_of_truth, loaded.sync.source_of_truth);
573 }
574 }
575}