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)]
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9#[cfg_attr(feature = "serde", serde(untagged))]
10pub enum Value {
11    Null,
12    Bool(bool),
13    Int(i64),
14    Float(f64),
15    String(String),
16    Array(Vec<Value>),
17    Object(HashMap<String, Value>),
18}
19
20impl Value {
21    /// `false`, `null`, `0`, `0.0`, and `""` are falsy. Everything else
22    /// (including empty arrays `[]`) is truthy.
23    pub fn is_truthy(&self) -> bool {
24        match self {
25            Value::Null => false,
26            Value::Bool(b) => *b,
27            Value::Int(i) => *i != 0,
28            Value::Float(f) => *f != 0.0,
29            Value::String(s) => !s.is_empty(),
30            Value::Array(_) => true,
31            Value::Object(_) => true,
32        }
33    }
34
35    pub fn is_null(&self) -> bool {
36        matches!(self, Value::Null)
37    }
38
39    pub fn type_name(&self) -> &'static str {
40        match self {
41            Value::Null => "null",
42            Value::Bool(_) => "bool",
43            Value::Int(_) => "int",
44            Value::Float(_) => "float",
45            Value::String(_) => "string",
46            Value::Array(_) => "array",
47            Value::Object(_) => "object",
48        }
49    }
50
51    /// Returns the display string for this value (used in interpolation).
52    pub fn to_display_string(&self) -> String {
53        match self {
54            Value::Null => "null".to_string(),
55            Value::Bool(b) => b.to_string(),
56            Value::Int(i) => i.to_string(),
57            Value::Float(f) => {
58                if f.fract() == 0.0 && f.abs() < 1e15 {
59                    format!("{}", *f as i64)
60                } else {
61                    f.to_string()
62                }
63            }
64            Value::String(s) => s.clone(),
65            Value::Array(arr) => {
66                let parts: Vec<String> = arr.iter().map(|v| v.to_display_string()).collect();
67                parts.join(",")
68            }
69            Value::Object(_) => "[object Object]".to_string(),
70        }
71    }
72
73    /// Serialises the value as a JSON string.
74    pub fn to_json_string(&self) -> String {
75        match self {
76            Value::Null => "null".to_string(),
77            Value::Bool(b) => b.to_string(),
78            Value::Int(i) => i.to_string(),
79            Value::Float(f) => f.to_string(),
80            Value::String(s) => {
81                let mut out = String::with_capacity(s.len() + 2);
82                out.push('"');
83                for c in s.chars() {
84                    match c {
85                        '"' => out.push_str("\\\""),
86                        '\\' => out.push_str("\\\\"),
87                        '\n' => out.push_str("\\n"),
88                        '\r' => out.push_str("\\r"),
89                        '\t' => out.push_str("\\t"),
90                        c => out.push(c),
91                    }
92                }
93                out.push('"');
94                out
95            }
96            Value::Array(arr) => {
97                let parts: Vec<String> = arr.iter().map(|v| v.to_json_string()).collect();
98                format!("[{}]", parts.join(","))
99            }
100            Value::Object(obj) => {
101                let mut pairs: Vec<String> = obj
102                    .iter()
103                    .map(|(k, v)| format!("\"{}\":{}", json_escape_str(k), v.to_json_string()))
104                    .collect();
105                pairs.sort(); // deterministic output
106                format!("{{{}}}", pairs.join(","))
107            }
108        }
109    }
110
111    /// Returns the HTML-escaped display string (safe for insertion into HTML).
112    pub fn html_escaped(&self) -> String {
113        html_escape(&self.to_display_string())
114    }
115
116    pub fn length(&self) -> Option<usize> {
117        match self {
118            Value::String(s) => Some(s.chars().count()),
119            Value::Array(a) => Some(a.len()),
120            Value::Object(o) => Some(o.len()),
121            _ => None,
122        }
123    }
124
125    pub fn is_empty(&self) -> bool {
126        match self {
127            Value::String(s) => s.is_empty(),
128            Value::Array(a) => a.is_empty(),
129            Value::Object(o) => o.is_empty(),
130            _ => false,
131        }
132    }
133}
134
135/// HTML-escapes `&`, `<`, `>`, `"`, `'`.
136pub fn html_escape(s: &str) -> String {
137    let mut out = String::with_capacity(s.len());
138    for c in s.chars() {
139        match c {
140            '&' => out.push_str("&amp;"),
141            '<' => out.push_str("&lt;"),
142            '>' => out.push_str("&gt;"),
143            '"' => out.push_str("&quot;"),
144            '\'' => out.push_str("&#x27;"),
145            c => out.push(c),
146        }
147    }
148    out
149}
150
151/// URL percent-encodes a string (unreserved characters are left as-is).
152pub fn urlencode(s: &str) -> String {
153    let mut out = String::new();
154    for byte in s.bytes() {
155        match byte {
156            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
157                out.push(byte as char)
158            }
159            b => out.push_str(&format!("%{:02X}", b)),
160        }
161    }
162    out
163}
164
165fn json_escape_str(s: &str) -> String {
166    let mut out = String::with_capacity(s.len());
167    for c in s.chars() {
168        match c {
169            '"' => out.push_str("\\\""),
170            '\\' => out.push_str("\\\\"),
171            '\n' => out.push_str("\\n"),
172            '\r' => out.push_str("\\r"),
173            '\t' => out.push_str("\\t"),
174            c => out.push(c),
175        }
176    }
177    out
178}
179
180impl fmt::Display for Value {
181    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182        write!(f, "{}", self.to_display_string())
183    }
184}
185
186impl From<bool> for Value {
187    fn from(b: bool) -> Self {
188        Value::Bool(b)
189    }
190}
191impl From<i64> for Value {
192    fn from(i: i64) -> Self {
193        Value::Int(i)
194    }
195}
196impl From<f64> for Value {
197    fn from(f: f64) -> Self {
198        Value::Float(f)
199    }
200}
201impl From<String> for Value {
202    fn from(s: String) -> Self {
203        Value::String(s)
204    }
205}
206impl From<&str> for Value {
207    fn from(s: &str) -> Self {
208        Value::String(s.to_string())
209    }
210}
211impl From<Vec<Value>> for Value {
212    fn from(v: Vec<Value>) -> Self {
213        Value::Array(v)
214    }
215}
216impl From<HashMap<String, Value>> for Value {
217    fn from(m: HashMap<String, Value>) -> Self {
218        Value::Object(m)
219    }
220}
221
222#[cfg(feature = "serde")]
223impl Value {
224    /// Convert any [`serde::Serialize`] value into a gracile [`Value`].
225    ///
226    /// This is the bridge between Rust's type system and the template engine.
227    /// It goes through [`serde_json`] as an intermediate, so any type that
228    /// serialises to a JSON object can be used as a render context.
229    pub fn from_serialize<T: serde::Serialize>(val: &T) -> Self {
230        serde_json::to_value(val)
231            .map(Into::into)
232            .unwrap_or(Value::Null)
233    }
234}
235
236#[cfg(feature = "serde")]
237impl From<serde_json::Value> for Value {
238    fn from(v: serde_json::Value) -> Self {
239        match v {
240            serde_json::Value::Null => Value::Null,
241            serde_json::Value::Bool(b) => Value::Bool(b),
242            serde_json::Value::Number(n) => {
243                if let Some(i) = n.as_i64() {
244                    Value::Int(i)
245                } else {
246                    Value::Float(n.as_f64().unwrap_or(f64::NAN))
247                }
248            }
249            serde_json::Value::String(s) => Value::String(s),
250            serde_json::Value::Array(a) => Value::Array(a.into_iter().map(Into::into).collect()),
251            serde_json::Value::Object(o) => {
252                Value::Object(o.into_iter().map(|(k, v)| (k, v.into())).collect())
253            }
254        }
255    }
256}