Skip to main content

serializer/
schema.rs

1//! Schema definition and type hints for DX format
2
3use crate::error::{DxError, Result};
4
5/// Type hints for columns (%i, %s, %f, %b, %x, %#)
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7pub enum TypeHint {
8    /// Integer (%i)
9    Int,
10    /// String (%s)
11    String,
12    /// Float (%f)
13    Float,
14    /// Boolean (%b)
15    Bool,
16    /// Base62 Integer (%x) - DX ∞
17    Base62,
18    /// Auto-Increment (%#) - DX ∞
19    AutoIncrement,
20    /// Auto-detect (no hint)
21    Auto,
22}
23
24impl TypeHint {
25    /// Parse type hint from byte (i, s, f, b, x, #)
26    pub fn from_byte(b: u8) -> Result<Self> {
27        match b {
28            b'i' => Ok(TypeHint::Int),
29            b's' => Ok(TypeHint::String),
30            b'f' => Ok(TypeHint::Float),
31            b'b' => Ok(TypeHint::Bool),
32            b'x' => Ok(TypeHint::Base62),
33            b'#' => Ok(TypeHint::AutoIncrement),
34            _ => Err(DxError::InvalidTypeHint(format!(
35                "Unknown type hint: {}",
36                b as char
37            ))),
38        }
39    }
40
41    /// Convert to byte for encoding
42    pub fn to_byte(self) -> u8 {
43        match self {
44            TypeHint::Int => b'i',
45            TypeHint::String => b's',
46            TypeHint::Base62 => b'x',
47            TypeHint::AutoIncrement => b'#',
48            TypeHint::Float => b'f',
49            TypeHint::Bool => b'b',
50            TypeHint::Auto => b'a',
51        }
52    }
53
54    /// Get type name for display
55    pub fn name(self) -> &'static str {
56        match self {
57            TypeHint::Int => "int",
58            TypeHint::String => "string",
59            TypeHint::Base62 => "base62",
60            TypeHint::AutoIncrement => "auto-increment",
61            TypeHint::Float => "float",
62            TypeHint::Bool => "bool",
63            TypeHint::Auto => "auto",
64        }
65    }
66}
67
68/// Column definition in a schema
69#[derive(Debug, Clone, PartialEq)]
70pub struct Column {
71    /// Column name as it appears in the schema.
72    pub name: String,
73    /// Optional type hint used while parsing values.
74    pub type_hint: TypeHint,
75}
76
77impl Column {
78    /// Create a column definition with an explicit type hint.
79    pub fn new(name: String, type_hint: TypeHint) -> Self {
80        Self { name, type_hint }
81    }
82
83    /// Check if this is an anonymous auto-increment column (#)
84    pub fn is_anonymous_auto_increment(&self) -> bool {
85        self.name == "#" && self.type_hint == TypeHint::AutoIncrement
86    }
87}
88
89/// Schema for a table (defined by `=`)
90#[derive(Debug, Clone, PartialEq)]
91pub struct Schema {
92    /// Table or schema name.
93    pub name: String,
94    /// Ordered column definitions.
95    pub columns: Vec<Column>,
96}
97
98impl Schema {
99    /// Create an empty schema with the provided name.
100    pub fn new(name: String) -> Self {
101        Self {
102            name,
103            columns: Vec::new(),
104        }
105    }
106
107    /// Create a schema from a prebuilt ordered column list.
108    pub fn with_columns(name: String, columns: Vec<Column>) -> Self {
109        Self { name, columns }
110    }
111
112    /// Append a column definition to the schema.
113    pub fn add_column(&mut self, name: String, type_hint: TypeHint) {
114        self.columns.push(Column::new(name, type_hint));
115    }
116
117    /// Parse schema definition like: "id%i name%s km%f active%b"
118    pub fn parse_definition(name: String, def: &str) -> Result<Self> {
119        let mut schema = Schema::new(name);
120        let parts: Vec<&str> = def.split_whitespace().collect();
121
122        let mut i = 0;
123        while i < parts.len() {
124            let part = parts[i];
125
126            // Check if this part has a type hint (e.g., "id%i")
127            if let Some(pos) = part.find('%') {
128                let col_name = &part[..pos];
129                let type_char = part.as_bytes().get(pos + 1).ok_or_else(|| {
130                    DxError::SchemaError(format!("Missing type after % in '{}'", part))
131                })?;
132                let type_hint = TypeHint::from_byte(*type_char)?;
133                schema.add_column(col_name.to_string(), type_hint);
134            } else {
135                // No type hint - check next part
136                if i + 1 < parts.len() && parts[i + 1].starts_with('%') {
137                    let type_str = &parts[i + 1][1..];
138                    let type_hint = if type_str.is_empty() {
139                        TypeHint::Auto
140                    } else {
141                        TypeHint::from_byte(type_str.as_bytes()[0])?
142                    };
143                    schema.add_column(part.to_string(), type_hint);
144                    i += 1; // Skip the type hint part
145                } else {
146                    // No type hint - auto detect
147                    schema.add_column(part.to_string(), TypeHint::Auto);
148                }
149            }
150            i += 1;
151        }
152
153        if schema.columns.is_empty() {
154            return Err(DxError::SchemaError(
155                "Schema must have at least one column".to_string(),
156            ));
157        }
158
159        Ok(schema)
160    }
161
162    /// Get column index by name
163    pub fn column_index(&self, name: &str) -> Option<usize> {
164        self.columns.iter().position(|c| c.name == name)
165    }
166
167    /// Get column by index
168    pub fn column(&self, idx: usize) -> Option<&Column> {
169        self.columns.get(idx)
170    }
171
172    /// Number of columns
173    pub fn len(&self) -> usize {
174        self.columns.len()
175    }
176
177    /// Check if schema is empty
178    pub fn is_empty(&self) -> bool {
179        self.columns.is_empty()
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    #[test]
188    fn test_parse_schema() {
189        let schema =
190            Schema::parse_definition("users".to_string(), "id%i name%s age%i active%b score%f")
191                .unwrap();
192
193        assert_eq!(schema.name, "users");
194        assert_eq!(schema.columns.len(), 5);
195        assert_eq!(schema.columns[0].name, "id");
196        assert_eq!(schema.columns[0].type_hint, TypeHint::Int);
197        assert_eq!(schema.columns[1].name, "name");
198        assert_eq!(schema.columns[1].type_hint, TypeHint::String);
199    }
200
201    #[test]
202    fn test_type_hint_roundtrip() {
203        let hints = [
204            TypeHint::Int,
205            TypeHint::String,
206            TypeHint::Float,
207            TypeHint::Bool,
208        ];
209
210        for hint in hints {
211            let byte = hint.to_byte();
212            let parsed = TypeHint::from_byte(byte).unwrap();
213            assert_eq!(hint, parsed);
214        }
215    }
216}