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}