// Value data model for moon_tera.
//
// A JSON-like dynamic value used as the rendering context and the data flowing
// through the engine. Ported from Tera's `Value` (which itself mirrors
// serde_json::Value). We use MoonBit's native enum instead of serde.
//
// `Object` keeps insertion order (an array of key/value pairs) to match Tera's
// use of an index map — important for deterministic template rendering.
///|
/// A dynamic value used as the template rendering context and intermediate data.
pub(all) enum Value {
Null
Bool(Bool)
Int(Int)
Float(Double)
Str(String)
Array(Array[Value])
// Insertion-ordered key/value pairs (avoids depending on a hash map and
// matches Tera's ordered context).
Object(Array[(String, Value)])
} derive(Debug)
///|
/// Truthiness used by `{% if %}` and filters like `default`.
///
/// Following Tera/Jinja: `Null`, `Bool(false)`, `Int(0)`, `Float(0.0)`,
/// empty `Str`/`Array`/`Object` are falsy; everything else is truthy.
pub fn Value::is_truthy(self : Value) -> Bool {
match self {
Null => false
Bool(b) => b
Int(i) => i != 0
Float(f) => f != 0.0
Str(s) => s.length() > 0
Array(a) => a.length() > 0
Object(o) => o.length() > 0
}
}
///|
/// Construct a null value.
pub fn null_value() -> Value {
Null
}
///|
/// Construct a boolean value.
pub fn bool_value(b : Bool) -> Value {
Bool(b)
}
///|
/// Construct an integer value.
pub fn int_value(i : Int) -> Value {
Int(i)
}
///|
/// Construct a float value.
pub fn float_value(f : Double) -> Value {
Float(f)
}
///|
/// Construct a string value.
pub fn str_value(s : String) -> Value {
Str(s)
}
///|
/// Construct an array value.
pub fn array_value(a : Array[Value]) -> Value {
Array(a)
}
///|
/// Construct an object value from insertion-ordered key/value pairs.
pub fn object_value(o : Array[(String, Value)]) -> Value {
Object(o)
}
///|
/// Return a stable, human-readable description for diagnostics and tests.
pub fn Value::describe(self : Value) -> String {
match self {
Null => "Null"
Bool(value) => "Bool(" + value.to_string() + ")"
Int(value) => "Int(" + value.to_string() + ")"
Float(value) => "Float(" + value.to_string() + ")"
Str(value) => "Str(" + value + ")"
Array(_) => "Array(" + self.to_json_string() + ")"
Object(_) => "Object(" + self.to_json_string() + ")"
}
}
///|
fn json_escape(value : String) -> String {
let out = StringBuilder::new()
for char in value {
match char {
'"' => out.write_string("\\\"")
'\\' => out.write_string("\\\\")
'\n' => out.write_string("\\n")
'\r' => out.write_string("\\r")
'\t' => out.write_string("\\t")
_ => out.write_char(char)
}
}
out.to_string()
}
///|
fn join_json_parts(parts : Array[String]) -> String {
let out = StringBuilder::new()
for index, part in parts {
if index > 0 {
out.write_string(",")
}
out.write_string(part)
}
out.to_string()
}
///|
/// Encode a value as deterministic JSON while preserving object insertion order.
pub fn Value::to_json_string(self : Value) -> String {
match self {
Null => "null"
Bool(value) => value.to_string()
Int(value) => value.to_string()
Float(value) => value.to_string()
Str(value) => "\"" + json_escape(value) + "\""
Array(values) => {
let parts : Array[String] = []
for value in values {
parts.push(value.to_json_string())
}
"[" + join_json_parts(parts) + "]"
}
Object(entries) => {
let parts : Array[String] = []
for entry in entries {
let (key, value) = entry
parts.push("\"" + json_escape(key) + "\":" + value.to_json_string())
}
"{" + join_json_parts(parts) + "}"
}
}
}