pub mod legacy;
mod markdown;
use crate::{DrivenConfig, DrivenError, EditorConfig, Result};
use serializer::{DxDocument, DxLlmValue, DxSection, document_to_llm, llm_to_document};
pub use serializer as dx_serializer;
pub use markdown::{DxDocumentable, DxMarkdownConfig, DxMarkdownFormat, rules_to_dx_markdown};
pub use legacy::{LegacyConverter, LegacyFormat, LegacySerializable};
pub trait DxSerializable: Sized {
fn to_dx_llm(&self) -> Result<String>;
fn from_dx_llm(content: &str) -> Result<Self>;
fn to_dx_machine(&self) -> Result<Vec<u8>>;
fn from_dx_machine(data: &[u8]) -> Result<Self>;
}
impl DxSerializable for DrivenConfig {
fn to_dx_llm(&self) -> Result<String> {
let mut doc = DxDocument::new();
doc.context.insert(
"nm".to_string(),
DxLlmValue::Str("driven-config".to_string()),
);
doc.context
.insert("v".to_string(), DxLlmValue::Str(self.version.clone()));
doc.context.insert(
"editor".to_string(),
DxLlmValue::Str(self.default_editor.to_string()),
);
let mut editors_section = DxSection::new(vec!["name".to_string(), "enabled".to_string()]);
editors_section
.add_row(vec![
DxLlmValue::Str("cursor".to_string()),
DxLlmValue::Bool(self.editors.cursor),
])
.ok();
editors_section
.add_row(vec![
DxLlmValue::Str("copilot".to_string()),
DxLlmValue::Bool(self.editors.copilot),
])
.ok();
editors_section
.add_row(vec![
DxLlmValue::Str("windsurf".to_string()),
DxLlmValue::Bool(self.editors.windsurf),
])
.ok();
editors_section
.add_row(vec![
DxLlmValue::Str("claude".to_string()),
DxLlmValue::Bool(self.editors.claude),
])
.ok();
editors_section
.add_row(vec![
DxLlmValue::Str("aider".to_string()),
DxLlmValue::Bool(self.editors.aider),
])
.ok();
editors_section
.add_row(vec![
DxLlmValue::Str("cline".to_string()),
DxLlmValue::Bool(self.editors.cline),
])
.ok();
doc.sections.insert('e', editors_section);
let mut sync_section = DxSection::new(vec!["key".to_string(), "value".to_string()]);
sync_section
.add_row(vec![
DxLlmValue::Str("watch".to_string()),
DxLlmValue::Bool(self.sync.watch),
])
.ok();
sync_section
.add_row(vec![
DxLlmValue::Str("auto_convert".to_string()),
DxLlmValue::Bool(self.sync.auto_convert),
])
.ok();
sync_section
.add_row(vec![
DxLlmValue::Str("source".to_string()),
DxLlmValue::Str(self.sync.source_of_truth.clone()),
])
.ok();
doc.sections.insert('s', sync_section);
Ok(document_to_llm(&doc))
}
fn from_dx_llm(content: &str) -> Result<Self> {
let doc = llm_to_document(content)
.map_err(|e| DrivenError::Parse(format!("DX LLM parse error: {}", e)))?;
let mut config = DrivenConfig::default();
if let Some(v) = doc.context.get("v").or_else(|| doc.context.get("vr")) {
config.version = match v {
DxLlmValue::Str(s) => s.clone(),
DxLlmValue::Num(n) => {
if n.fract() == 0.0 {
format!("{}.0", *n as i64)
} else {
format!("{}", n)
}
}
_ => config.version.clone(),
};
}
if let Some(v) = doc.context.get("editor").or_else(|| doc.context.get("ed")) {
if let Some(s) = v.as_str() {
config.default_editor = parse_editor(s)?;
}
}
if let Some(section) = doc.sections.get(&'e') {
for row in §ion.rows {
if row.len() >= 2 {
if let (Some(name), Some(enabled)) = (row[0].as_str(), row[1].as_bool()) {
match name {
"cursor" => config.editors.cursor = enabled,
"copilot" => config.editors.copilot = enabled,
"windsurf" => config.editors.windsurf = enabled,
"claude" => config.editors.claude = enabled,
"aider" => config.editors.aider = enabled,
"cline" => config.editors.cline = enabled,
_ => {}
}
}
}
}
}
if let Some(section) = doc.sections.get(&'s') {
for row in §ion.rows {
if row.len() >= 2 {
if let Some(key) = row[0].as_str() {
match key {
"watch" => {
if let Some(v) = row[1].as_bool() {
config.sync.watch = v;
}
}
"auto_convert" => {
if let Some(v) = row[1].as_bool() {
config.sync.auto_convert = v;
}
}
"source" => {
config.sync.source_of_truth = match &row[1] {
DxLlmValue::Str(s) => s.clone(),
DxLlmValue::Num(n) => {
if n.fract() == 0.0 {
format!("{}", *n as i64)
} else {
format!("{}", n)
}
}
_ => config.sync.source_of_truth.clone(),
};
}
_ => {}
}
}
}
}
}
Ok(config)
}
fn to_dx_machine(&self) -> Result<Vec<u8>> {
let version_bytes = self.version.as_bytes();
let source_bytes = self.sync.source_of_truth.as_bytes();
let total_len = 12 + version_bytes.len() + source_bytes.len();
let mut buffer = Vec::with_capacity(total_len);
buffer.extend_from_slice(b"DRV1");
let editor_flags: u8 = (self.editors.cursor as u8)
| ((self.editors.copilot as u8) << 1)
| ((self.editors.windsurf as u8) << 2)
| ((self.editors.claude as u8) << 3)
| ((self.editors.aider as u8) << 4)
| ((self.editors.cline as u8) << 5);
buffer.push(editor_flags);
let sync_flags: u8 = (self.sync.watch as u8) | ((self.sync.auto_convert as u8) << 1);
buffer.push(sync_flags);
buffer.push(self.default_editor as u8);
buffer.push(0);
buffer.extend_from_slice(&(version_bytes.len() as u16).to_le_bytes());
buffer.extend_from_slice(&(source_bytes.len() as u16).to_le_bytes());
buffer.extend_from_slice(version_bytes);
buffer.extend_from_slice(source_bytes);
Ok(buffer)
}
fn from_dx_machine(data: &[u8]) -> Result<Self> {
if data.len() < 12 {
return Err(DrivenError::InvalidBinary(
"Data too short for DrivenConfig".to_string(),
));
}
if &data[0..4] != b"DRV1" {
return Err(DrivenError::InvalidBinary(
"Invalid magic bytes".to_string(),
));
}
let editor_flags = data[4];
let sync_flags = data[5];
let default_editor_byte = data[6];
let version_len = u16::from_le_bytes([data[8], data[9]]) as usize;
let source_len = u16::from_le_bytes([data[10], data[11]]) as usize;
if data.len() < 12 + version_len + source_len {
return Err(DrivenError::InvalidBinary(
"Data too short for strings".to_string(),
));
}
let version = std::str::from_utf8(&data[12..12 + version_len])
.map_err(|e| DrivenError::InvalidBinary(format!("Invalid UTF-8 in version: {}", e)))?
.to_string();
let source_of_truth =
std::str::from_utf8(&data[12 + version_len..12 + version_len + source_len])
.map_err(|e| DrivenError::InvalidBinary(format!("Invalid UTF-8 in source: {}", e)))?
.to_string();
Ok(DrivenConfig {
version,
default_editor: editor_from_byte(default_editor_byte)?,
editors: EditorConfig {
cursor: (editor_flags & 1) != 0,
copilot: (editor_flags & 2) != 0,
windsurf: (editor_flags & 4) != 0,
claude: (editor_flags & 8) != 0,
aider: (editor_flags & 16) != 0,
cline: (editor_flags & 32) != 0,
},
sync: crate::SyncConfig {
watch: (sync_flags & 1) != 0,
auto_convert: (sync_flags & 2) != 0,
source_of_truth,
},
templates: crate::TemplateConfig::default(),
context: crate::ContextConfig::default(),
})
}
}
#[allow(dead_code)]
fn read_slot_string(_data: &[u8], _slot_offset: usize) -> Result<String> {
Err(DrivenError::InvalidBinary("Not implemented".to_string()))
}
fn parse_editor(s: &str) -> Result<crate::Editor> {
match s.to_lowercase().as_str() {
"cursor" => Ok(crate::Editor::Cursor),
"copilot" | "github copilot" => Ok(crate::Editor::Copilot),
"windsurf" => Ok(crate::Editor::Windsurf),
"claude" | "claude code" => Ok(crate::Editor::Claude),
"aider" => Ok(crate::Editor::Aider),
"cline" => Ok(crate::Editor::Cline),
_ => Err(DrivenError::Parse(format!("Unknown editor: {}", s))),
}
}
fn editor_from_byte(byte: u8) -> Result<crate::Editor> {
match byte {
0 => Ok(crate::Editor::Cursor),
1 => Ok(crate::Editor::Copilot),
2 => Ok(crate::Editor::Windsurf),
3 => Ok(crate::Editor::Claude),
4 => Ok(crate::Editor::Aider),
5 => Ok(crate::Editor::Cline),
_ => Err(DrivenError::InvalidBinary(format!(
"Unknown editor byte: {}",
byte
))),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_driven_config_dx_llm_roundtrip() {
let config = DrivenConfig::default();
let llm = config.to_dx_llm().unwrap();
println!("Serialized LLM:\n{}", llm);
let loaded = DrivenConfig::from_dx_llm(&llm).unwrap();
assert_eq!(config.version, loaded.version);
assert_eq!(config.editors.cursor, loaded.editors.cursor);
assert_eq!(config.editors.copilot, loaded.editors.copilot);
assert_eq!(config.sync.watch, loaded.sync.watch);
}
#[test]
fn test_driven_config_dx_llm_roundtrip_numeric_version() {
let mut config = DrivenConfig::default();
config.version = "0.0".to_string();
let llm = config.to_dx_llm().unwrap();
println!("Serialized LLM:\n{}", llm);
let loaded = DrivenConfig::from_dx_llm(&llm).unwrap();
println!("Loaded version: {}", loaded.version);
assert_eq!(config.version, loaded.version);
}
#[test]
fn test_driven_config_dx_machine_roundtrip() {
let config = DrivenConfig::default();
let binary = config.to_dx_machine().unwrap();
let loaded = DrivenConfig::from_dx_machine(&binary).unwrap();
assert_eq!(config.version, loaded.version);
assert_eq!(config.editors.cursor, loaded.editors.cursor);
assert_eq!(config.editors.copilot, loaded.editors.copilot);
assert_eq!(config.sync.watch, loaded.sync.watch);
}
}
#[cfg(test)]
mod prop_tests {
use super::*;
use proptest::prelude::*;
fn arb_editor() -> impl Strategy<Value = crate::Editor> {
prop_oneof![
Just(crate::Editor::Cursor),
Just(crate::Editor::Copilot),
Just(crate::Editor::Windsurf),
Just(crate::Editor::Claude),
Just(crate::Editor::Aider),
Just(crate::Editor::Cline),
]
}
fn arb_editor_config() -> impl Strategy<Value = EditorConfig> {
(
any::<bool>(),
any::<bool>(),
any::<bool>(),
any::<bool>(),
any::<bool>(),
any::<bool>(),
)
.prop_map(
|(cursor, copilot, windsurf, claude, aider, cline)| EditorConfig {
cursor,
copilot,
windsurf,
claude,
aider,
cline,
},
)
}
fn arb_sync_config() -> impl Strategy<Value = crate::SyncConfig> {
(any::<bool>(), any::<bool>(), "[a-zA-Z0-9_./]{1,50}").prop_map(
|(watch, auto_convert, source)| crate::SyncConfig {
watch,
auto_convert,
source_of_truth: source,
},
)
}
fn arb_driven_config() -> impl Strategy<Value = DrivenConfig> {
(
"[0-9]+\\.[0-9]+\\.[0-9]+",
arb_editor(),
arb_editor_config(),
arb_sync_config(),
)
.prop_map(|(version, default_editor, editors, sync)| DrivenConfig {
version: format!("v{}", version), default_editor,
editors,
sync,
templates: crate::TemplateConfig::default(),
context: crate::ContextConfig::default(),
})
}
proptest! {
#[test]
fn prop_dx_llm_roundtrip(config in arb_driven_config()) {
let llm = config.to_dx_llm().expect("Serialization should succeed");
let loaded = DrivenConfig::from_dx_llm(&llm).expect("Deserialization should succeed");
prop_assert_eq!(config.version, loaded.version);
prop_assert_eq!(config.default_editor, loaded.default_editor);
prop_assert_eq!(config.editors.cursor, loaded.editors.cursor);
prop_assert_eq!(config.editors.copilot, loaded.editors.copilot);
prop_assert_eq!(config.editors.windsurf, loaded.editors.windsurf);
prop_assert_eq!(config.editors.claude, loaded.editors.claude);
prop_assert_eq!(config.editors.aider, loaded.editors.aider);
prop_assert_eq!(config.editors.cline, loaded.editors.cline);
prop_assert_eq!(config.sync.watch, loaded.sync.watch);
prop_assert_eq!(config.sync.auto_convert, loaded.sync.auto_convert);
prop_assert_eq!(config.sync.source_of_truth, loaded.sync.source_of_truth);
}
#[test]
fn prop_dx_machine_roundtrip(config in arb_driven_config()) {
let binary = config.to_dx_machine().expect("Serialization should succeed");
let loaded = DrivenConfig::from_dx_machine(&binary).expect("Deserialization should succeed");
prop_assert_eq!(config.version, loaded.version);
prop_assert_eq!(config.default_editor, loaded.default_editor);
prop_assert_eq!(config.editors.cursor, loaded.editors.cursor);
prop_assert_eq!(config.editors.copilot, loaded.editors.copilot);
prop_assert_eq!(config.editors.windsurf, loaded.editors.windsurf);
prop_assert_eq!(config.editors.claude, loaded.editors.claude);
prop_assert_eq!(config.editors.aider, loaded.editors.aider);
prop_assert_eq!(config.editors.cline, loaded.editors.cline);
prop_assert_eq!(config.sync.watch, loaded.sync.watch);
prop_assert_eq!(config.sync.auto_convert, loaded.sync.auto_convert);
prop_assert_eq!(config.sync.source_of_truth, loaded.sync.source_of_truth);
}
}
}