Skip to main content

gracile_core/
value.rs

1//! Runtime value types used during template evaluation.
2
3use std::collections::HashMap;
4use std::fmt;
5
6/// A runtime value in the Gracile template engine.
7#[derive(Debug, Clone, PartialEq)]
8pub enum Value {
9    Null,
10    Bool(bool),
11    Int(i64),
12    Float(f64),
13    String(String),
14    Array(Vec<Value>),
15    Object(HashMap<String, Value>),
16}
17
18impl Value {
19    /// `false`, `null`, `0`, `0.0`, and `""` are falsy. Everything else
20    /// (including empty arrays `[]`) is truthy.
21    pub fn is_truthy(&self) -> bool {
22        match self {
23            Value::Null => false,
24            Value::Bool(b) => *b,
25            Value::Int(i) => *i != 0,
26            Value::Float(f) => *f != 0.0,
27            Value::String(s) => !s.is_empty(),
28            Value::Array(_) => true,
29            Value::Object(_) => true,
30        }
31    }
32
33    pub fn is_null(&self) -> bool {
34        matches!(self, Value::Null)
35    }
36
37    pub fn type_name(&self) -> &'static str {
38        match self {
39            Value::Null => "null",
40            Value::Bool(_) => "bool",
41            Value::Int(_) => "int",
42            Value::Float(_) => "float",
43            Value::String(_) => "string",
44            Value::Array(_) => "array",
45            Value::Object(_) => "object",
46        }
47    }
48
49    /// Returns the display string for this value (used in interpolation).
50    pub fn to_display_string(&self) -> String {
51        match self {
52            Value::Null => "null".to_string(),
53            Value::Bool(b) => b.to_string(),
54            Value::Int(i) => i.to_string(),
55            Value::Float(f) => {
56                if f.fract() == 0.0 && f.abs() < 1e15 {
57                    format!("{}", *f as i64)
58                } else {
59                    f.to_string()
60                }
61            }
62            Value::String(s) => s.clone(),
63            Value::Array(arr) => {
64                let parts: Vec<String> = arr.iter().map(|v| v.to_display_string()).collect();
65                parts.join(",")
66            }
67            Value::Object(_) => "[object Object]".to_string(),
68        }
69    }
70
71    /// Serialises the value as a JSON string.
72    pub fn to_json_string(&self) -> String {
73        match self {
74            Value::Null => "null".to_string(),
75            Value::Bool(b) => b.to_string(),
76            Value::Int(i) => i.to_string(),
77            Value::Float(f) => f.to_string(),
78            Value::String(s) => {
79                let mut out = String::with_capacity(s.len() + 2);
80                out.push('"');
81                for c in s.chars() {
82                    match c {
83                        '"' => out.push_str("\\\""),
84                        '\\' => out.push_str("\\\\"),
85                        '\n' => out.push_str("\\n"),
86                        '\r' => out.push_str("\\r"),
87                        '\t' => out.push_str("\\t"),
88                        c => out.push(c),
89                    }
90                }
91                out.push('"');
92                out
93            }
94            Value::Array(arr) => {
95                let parts: Vec<String> = arr.iter().map(|v| v.to_json_string()).collect();
96                format!("[{}]", parts.join(","))
97            }
98            Value::Object(obj) => {
99                let mut pairs: Vec<String> = obj
100                    .iter()
101                    .map(|(k, v)| format!("\"{}\":{}", json_escape_str(k), v.to_json_string()))
102                    .collect();
103                pairs.sort(); // deterministic output
104                format!("{{{}}}", pairs.join(","))
105            }
106        }
107    }
108
109    /// Returns the HTML-escaped display string (safe for insertion into HTML).
110    pub fn html_escaped(&self) -> String {
111        html_escape(&self.to_display_string())
112    }
113
114    pub fn length(&self) -> Option<usize> {
115        match self {
116            Value::String(s) => Some(s.chars().count()),
117            Value::Array(a) => Some(a.len()),
118            Value::Object(o) => Some(o.len()),
119            _ => None,
120        }
121    }
122
123    pub fn is_empty(&self) -> bool {
124        match self {
125            Value::String(s) => s.is_empty(),
126            Value::Array(a) => a.is_empty(),
127            Value::Object(o) => o.is_empty(),
128            _ => false,
129        }
130    }
131}
132
133/// HTML-escapes `&`, `<`, `>`, `"`, `'`.
134pub fn html_escape(s: &str) -> String {
135    let mut out = String::with_capacity(s.len());
136    for c in s.chars() {
137        match c {
138            '&' => out.push_str("&amp;"),
139            '<' => out.push_str("&lt;"),
140            '>' => out.push_str("&gt;"),
141            '"' => out.push_str("&quot;"),
142            '\'' => out.push_str("&#x27;"),
143            c => out.push(c),
144        }
145    }
146    out
147}
148
149/// URL percent-encodes a string (unreserved characters are left as-is).
150pub fn urlencode(s: &str) -> String {
151    let mut out = String::new();
152    for byte in s.bytes() {
153        match byte {
154            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
155                out.push(byte as char)
156            }
157            b => out.push_str(&format!("%{:02X}", b)),
158        }
159    }
160    out
161}
162
163fn json_escape_str(s: &str) -> String {
164    let mut out = String::with_capacity(s.len());
165    for c in s.chars() {
166        match c {
167            '"' => out.push_str("\\\""),
168            '\\' => out.push_str("\\\\"),
169            '\n' => out.push_str("\\n"),
170            '\r' => out.push_str("\\r"),
171            '\t' => out.push_str("\\t"),
172            c => out.push(c),
173        }
174    }
175    out
176}
177
178impl fmt::Display for Value {
179    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180        write!(f, "{}", self.to_display_string())
181    }
182}
183
184impl From<bool> for Value {
185    fn from(b: bool) -> Self {
186        Value::Bool(b)
187    }
188}
189impl From<i64> for Value {
190    fn from(i: i64) -> Self {
191        Value::Int(i)
192    }
193}
194impl From<f64> for Value {
195    fn from(f: f64) -> Self {
196        Value::Float(f)
197    }
198}
199impl From<String> for Value {
200    fn from(s: String) -> Self {
201        Value::String(s)
202    }
203}
204impl From<&str> for Value {
205    fn from(s: &str) -> Self {
206        Value::String(s.to_string())
207    }
208}
209impl From<Vec<Value>> for Value {
210    fn from(v: Vec<Value>) -> Self {
211        Value::Array(v)
212    }
213}
214impl From<HashMap<String, Value>> for Value {
215    fn from(m: HashMap<String, Value>) -> Self {
216        Value::Object(m)
217    }
218}