Skip to main content

gram_codec/
value.rs

1//! Value enum for heterogeneous property types in Gram notation
2
3use std::fmt;
4
5/// Represents all possible value types in Gram notation property records.
6/// Supports all value types defined in the tree-sitter-gram grammar.
7#[derive(Debug, Clone)]
8pub enum Value {
9    /// Unquoted or quoted string
10    /// Example: `"Alice"`, `hello`
11    String(String),
12
13    /// Integer value with full i64 range
14    /// Example: `42`, `-10`, `0`
15    Integer(i64),
16
17    /// Decimal/floating-point value
18    /// Example: `3.14`, `-2.5`, `0.0`
19    Decimal(f64),
20
21    /// Boolean value
22    /// Example: `true`, `false`
23    Boolean(bool),
24
25    /// Array of values (may be heterogeneous)
26    /// Example: `["rust", 42, true]`
27    Array(Vec<Value>),
28
29    /// Numeric range with inclusive bounds
30    /// Example: `1..10`, `0..100`
31    Range { lower: i64, upper: i64 },
32
33    /// Tagged string with format identifier
34    /// Example: `"""markdown # Heading"""`
35    TaggedString { tag: String, content: String },
36}
37
38impl Value {
39    // TODO: tree-sitter parsing methods removed during nom parser migration
40    // Value parsing is now handled by parser::value module
41
42    /* Commented out during migration to nom parser
43    pub fn from_tree_sitter_node_OLD(
44        node: &TREE_SITTER_NODE,
45        source: &str,
46    ) -> Result<Self, ParseError> {
47        match node.kind() {
48            "symbol" => {
49                let text = node
50                    .utf8_text(source.as_bytes())
51                    .map_err(|e| Self::node_parse_error(node, format!("UTF-8 error: {}", e)))?;
52                Ok(Value::String(text.to_string()))
53            }
54            "string_literal" => {
55                let content = extract_string_content(node, source)?;
56                Ok(Value::String(content))
57            }
58            "integer" => {
59                let text = node
60                    .utf8_text(source.as_bytes())
61                    .map_err(|e| Self::node_parse_error(node, format!("UTF-8 error: {}", e)))?;
62                let value = text
63                    .parse::<i64>()
64                    .map_err(|e| Self::node_parse_error(node, format!("Invalid integer: {}", e)))?;
65                Ok(Value::Integer(value))
66            }
67            "decimal" => {
68                let text = node
69                    .utf8_text(source.as_bytes())
70                    .map_err(|e| Self::node_parse_error(node, format!("UTF-8 error: {}", e)))?;
71                let value = text
72                    .parse::<f64>()
73                    .map_err(|e| Self::node_parse_error(node, format!("Invalid decimal: {}", e)))?;
74                Ok(Value::Decimal(value))
75            }
76            "boolean_literal" => {
77                let text = node
78                    .utf8_text(source.as_bytes())
79                    .map_err(|e| Self::node_parse_error(node, format!("UTF-8 error: {}", e)))?;
80                let value = text == "true";
81                Ok(Value::Boolean(value))
82            }
83            "array" => {
84                let mut values = Vec::new();
85                let mut cursor = node.walk();
86                for child in node.children(&mut cursor) {
87                    if child.is_named() && child.kind() != "," {
88                        values.push(Value::from_tree_sitter_node(&child, source)?);
89                    }
90                }
91                Ok(Value::Array(values))
92            }
93            "range" => {
94                let lower = extract_range_bound(node, "lower", source)?;
95                let upper = extract_range_bound(node, "upper", source)?;
96                Ok(Value::Range { lower, upper })
97            }
98            "tagged_string" => {
99                let (tag, content) = extract_tagged_string(node, source)?;
100                Ok(Value::TaggedString { tag, content })
101            }
102            _ => panic!("tree-sitter parsing no longer supported"),
103        }
104    }
105    */
106    // End of commented tree-sitter code
107
108    /// Serialize value to gram notation
109    pub fn to_gram_notation(&self) -> String {
110        match self {
111            // Strings are always quoted in gram notation property values
112            Value::String(s) => format!("\"{}\"", escape_string(s)),
113            Value::Integer(i) => i.to_string(),
114            Value::Decimal(f) => format_decimal(*f),
115            Value::Boolean(b) => b.to_string(),
116            Value::Array(values) => {
117                let items: Vec<String> = values.iter().map(|v| v.to_gram_notation()).collect();
118                format!("[{}]", items.join(", "))
119            }
120            Value::Range { lower, upper } => format!("{}..{}", lower, upper),
121            Value::TaggedString { tag, content } => {
122                if tag.is_empty() {
123                    format!("\"\"\"{}\"\"\"", content)
124                } else {
125                    // Use backtick format: tag`content` (the format the parser actually handles).
126                    // Escape backslashes first, then backticks — order matters: if backticks were
127                    // escaped first, the newly inserted backslashes would be double-escaped on the
128                    // second pass, corrupting content that contains a literal backslash before a
129                    // backtick (e.g. "\`" → "\\`" → "\\\`" instead of "\\\\`").
130                    let escaped = content.replace('\\', "\\\\").replace('`', "\\`");
131                    format!("{}`{}`", tag, escaped)
132                }
133            }
134        }
135    }
136
137    /// Get the type name of this value (for error messages)
138    pub fn type_name(&self) -> &'static str {
139        match self {
140            Value::String(_) => "string",
141            Value::Integer(_) => "integer",
142            Value::Decimal(_) => "decimal",
143            Value::Boolean(_) => "boolean",
144            Value::Array(_) => "array",
145            Value::Range { .. } => "range",
146            Value::TaggedString { .. } => "tagged string",
147        }
148    }
149
150    // TODO: node_parse_error removed during migration
151}
152
153impl fmt::Display for Value {
154    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155        write!(f, "{}", self.to_gram_notation())
156    }
157}
158
159impl PartialEq for Value {
160    fn eq(&self, other: &Self) -> bool {
161        match (self, other) {
162            (Value::String(a), Value::String(b)) => a == b,
163            (Value::Integer(a), Value::Integer(b)) => a == b,
164            (Value::Decimal(a), Value::Decimal(b)) => {
165                // Use epsilon comparison for floats
166                (a - b).abs() < f64::EPSILON
167            }
168            (Value::Boolean(a), Value::Boolean(b)) => a == b,
169            (Value::Array(a), Value::Array(b)) => a == b,
170            (
171                Value::Range {
172                    lower: l1,
173                    upper: u1,
174                },
175                Value::Range {
176                    lower: l2,
177                    upper: u2,
178                },
179            ) => l1 == l2 && u1 == u2,
180            (
181                Value::TaggedString {
182                    tag: t1,
183                    content: c1,
184                },
185                Value::TaggedString {
186                    tag: t2,
187                    content: c2,
188                },
189            ) => t1 == t2 && c1 == c2,
190            _ => false,
191        }
192    }
193}
194
195// Helper Functions
196
197/// Escape special characters in strings
198pub(crate) fn escape_string(s: &str) -> String {
199    s.replace('\\', "\\\\")
200        .replace('"', "\\\"")
201        .replace('\n', "\\n")
202        .replace('\t', "\\t")
203        .replace('\r', "\\r")
204}
205
206/// Format decimal to avoid unnecessary trailing zeros while distinguishing from integers
207pub(crate) fn format_decimal(f: f64) -> String {
208    if f.fract() == 0.0 && f.is_finite() {
209        format!("{:.1}", f) // Always include .0 to distinguish from integer
210    } else {
211        f.to_string()
212    }
213}
214
215// TODO: tree-sitter helper functions removed during migration to nom parser
216// Value parsing is now handled by parser::value module
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    #[test]
223    fn test_string_value_serialization() {
224        // Strings are always quoted in gram notation property values
225        assert_eq!(
226            Value::String("hello".to_string()).to_gram_notation(),
227            "\"hello\""
228        );
229        assert_eq!(
230            Value::String("Hello World".to_string()).to_gram_notation(),
231            "\"Hello World\""
232        );
233        assert_eq!(Value::String("".to_string()).to_gram_notation(), "\"\"");
234    }
235
236    #[test]
237    fn test_integer_value_serialization() {
238        assert_eq!(Value::Integer(42).to_gram_notation(), "42");
239        assert_eq!(Value::Integer(-10).to_gram_notation(), "-10");
240        assert_eq!(Value::Integer(0).to_gram_notation(), "0");
241    }
242
243    #[test]
244    fn test_decimal_value_serialization() {
245        assert_eq!(Value::Decimal(3.14).to_gram_notation(), "3.14");
246        assert_eq!(Value::Decimal(0.0).to_gram_notation(), "0.0");
247        assert_eq!(Value::Decimal(-2.5).to_gram_notation(), "-2.5");
248    }
249
250    #[test]
251    fn test_boolean_value_serialization() {
252        assert_eq!(Value::Boolean(true).to_gram_notation(), "true");
253        assert_eq!(Value::Boolean(false).to_gram_notation(), "false");
254    }
255
256    #[test]
257    fn test_array_value_serialization() {
258        let v = Value::Array(vec![
259            Value::Integer(1),
260            Value::Integer(2),
261            Value::Integer(3),
262        ]);
263        assert_eq!(v.to_gram_notation(), "[1, 2, 3]");
264
265        // Heterogeneous array
266        let v = Value::Array(vec![
267            Value::String("rust".to_string()),
268            Value::Integer(42),
269            Value::Boolean(true),
270        ]);
271        // Strings are always quoted
272        assert_eq!(v.to_gram_notation(), "[\"rust\", 42, true]");
273
274        // Empty array
275        assert_eq!(Value::Array(vec![]).to_gram_notation(), "[]");
276    }
277
278    #[test]
279    fn test_range_value_serialization() {
280        let v = Value::Range {
281            lower: 1,
282            upper: 10,
283        };
284        assert_eq!(v.to_gram_notation(), "1..10");
285
286        let v = Value::Range {
287            lower: -5,
288            upper: 5,
289        };
290        assert_eq!(v.to_gram_notation(), "-5..5");
291    }
292
293    #[test]
294    fn test_tagged_string_serialization() {
295        let v = Value::TaggedString {
296            tag: "markdown".to_string(),
297            content: "# Heading".to_string(),
298        };
299        assert_eq!(v.to_gram_notation(), "markdown`# Heading`");
300
301        let v = Value::TaggedString {
302            tag: String::new(),
303            content: "Plain text".to_string(),
304        };
305        assert_eq!(v.to_gram_notation(), "\"\"\"Plain text\"\"\"");
306
307        // Verify backtick and backslash escaping in content
308        let v = Value::TaggedString {
309            tag: "h3".to_string(),
310            content: "8f283082aa20c00".to_string(),
311        };
312        assert_eq!(v.to_gram_notation(), "h3`8f283082aa20c00`");
313
314        let v = Value::TaggedString {
315            tag: "raw".to_string(),
316            content: "has`backtick".to_string(),
317        };
318        assert_eq!(v.to_gram_notation(), "raw`has\\`backtick`");
319
320        let v = Value::TaggedString {
321            tag: "raw".to_string(),
322            content: "has\\backslash".to_string(),
323        };
324        assert_eq!(v.to_gram_notation(), "raw`has\\\\backslash`");
325
326        // Backslash immediately before a backtick: both must be escaped independently.
327        let v = Value::TaggedString {
328            tag: "raw".to_string(),
329            content: "end\\`here".to_string(),
330        };
331        assert_eq!(v.to_gram_notation(), "raw`end\\\\\\`here`");
332    }
333
334    #[test]
335    fn test_value_type_names() {
336        assert_eq!(Value::String("".to_string()).type_name(), "string");
337        assert_eq!(Value::Integer(0).type_name(), "integer");
338        assert_eq!(Value::Decimal(0.0).type_name(), "decimal");
339        assert_eq!(Value::Boolean(false).type_name(), "boolean");
340        assert_eq!(Value::Array(vec![]).type_name(), "array");
341        assert_eq!(Value::Range { lower: 0, upper: 0 }.type_name(), "range");
342        assert_eq!(
343            Value::TaggedString {
344                tag: String::new(),
345                content: String::new()
346            }
347            .type_name(),
348            "tagged string"
349        );
350    }
351
352    #[test]
353    fn test_value_equality() {
354        assert_eq!(Value::Integer(42), Value::Integer(42));
355        assert_ne!(Value::Integer(42), Value::Integer(43));
356        assert_ne!(Value::Integer(42), Value::String("42".to_string()));
357
358        // Decimal epsilon comparison
359        assert_eq!(Value::Decimal(1.0), Value::Decimal(1.0));
360
361        // Arrays
362        assert_eq!(
363            Value::Array(vec![Value::Integer(1), Value::Integer(2)]),
364            Value::Array(vec![Value::Integer(1), Value::Integer(2)])
365        );
366    }
367
368    #[test]
369    fn test_escape_string() {
370        assert_eq!(escape_string("hello"), "hello");
371        assert_eq!(escape_string("hello\"world"), "hello\\\"world");
372        assert_eq!(escape_string("line1\nline2"), "line1\\nline2");
373        assert_eq!(escape_string("tab\there"), "tab\\there");
374        assert_eq!(escape_string("back\\slash"), "back\\\\slash");
375    }
376
377    #[test]
378    fn test_format_decimal() {
379        assert_eq!(format_decimal(3.14), "3.14");
380        assert_eq!(format_decimal(0.0), "0.0");
381        assert_eq!(format_decimal(1.0), "1.0");
382        assert_eq!(format_decimal(-2.5), "-2.5");
383    }
384}