///|
let builtin_names : Array[String] = [
"lowercase", "uppercase", "strip", "lstrip", "rstrip", "len", "str", "number",
"join", "split", "typeof", "defined", "min", "max", "sqrt", "abs", "ceil", "floor",
"range", "fromNow",
]
///|
fn environment(
context : Json,
b : Budget,
now : String,
) -> Map[String, Value] raise TemplateError {
let env = Map([])
for name in builtin_names {
env[name] = Function(name)
}
env["now"] = Text(now)
match from_json(context, b, 0) {
Record(o) =>
for k, v in o {
if !identifier(k) {
raise Invalid("invalid context key: " + k)
}
env[k] = v
}
_ => raise Invalid("context must be an object")
}
env
}
///|
fn arity(
args : Array[Value],
minimum : Int32,
maximum : Int32,
) -> Unit raise TemplateError {
if args.length() < minimum || args.length() > maximum {
raise Invalid("invalid builtin argument count")
}
}
///|
fn builtin(
name : String,
args : Array[Value],
env : Map[String, Value],
b : Budget,
) -> Value raise TemplateError {
b.tick(0)
match name {
"min" | "max" => {
arity(args, 1, b.max_items)
let mut result = numeric(args[0])
for x in args {
let n = numeric(x)
if (name == "min" && n < result) || (name == "max" && n > result) {
result = n
}
}
Numeric(result)
}
"abs" | "sqrt" | "ceil" | "floor" => {
arity(args, 1, 1)
let n = numeric(args[0])
let result = match name {
"abs" => if n < 0 { -n } else { n }
"ceil" => @math.ceil(n)
"floor" => @math.floor(n)
_ => {
if n < 0 {
raise Invalid("sqrt of negative number")
}
n.sqrt()
}
}
Numeric(result)
}
"range" => {
arity(args, 2, 3)
let start = index_value(args[0])
let stop = index_value(args[1])
let step = if args.length() == 3 { index_value(args[2]) } else { 1 }
if step == 0 {
raise Invalid("range step is zero")
}
let out = []
let mut n = start.to_double()
while (step > 0 && n < stop.to_double()) ||
(step < 0 && n > stop.to_double()) {
b.tick(0)
b.items(out.length() + 1)
out.push(Numeric(n))
n += step.to_double()
}
Sequence(out)
}
"lowercase" | "uppercase" | "strip" | "lstrip" | "rstrip" => {
arity(args, 1, 1)
let s = string_value(args[0])
Text(
match name {
"lowercase" => s.to_lower()
"uppercase" => s.to_upper()
"strip" => s.trim(chars=" \t\n\r\u{0b}\u{0c}").to_owned()
"lstrip" => s.trim_start(chars=" \t\n\r\u{0b}\u{0c}").to_owned()
_ => s.trim_end(chars=" \t\n\r\u{0b}\u{0c}").to_owned()
},
)
}
"len" => {
arity(args, 1, 1)
let n = match args[0] {
Text(s) => s.to_array().length()
Sequence(xs) => xs.length()
_ => raise Invalid("len requires string or array")
}
Numeric(n.to_double())
}
"str" => {
arity(args, 1, 1)
Text(text(args[0]))
}
"number" => {
arity(args, 1, 1)
let s = string_value(args[0]).trim().to_owned()
let n = @string.parse_double(s[:]) catch {
_ => raise Invalid("invalid numeric string")
}
if n.is_nan() || n.is_inf() {
raise Invalid("non-finite number")
}
Numeric(n)
}
"join" => {
arity(args, 2, 2)
let sep = string_or_number(args[1])
let xs = match args[0] {
Sequence(xs) => xs
_ => raise Invalid("join requires array")
}
let out = StringBuilder()
let mut size = 0
for i, x in xs {
if i > 0 {
out.write_string(sep)
}
let part = string_or_number(x)
size += part.length() + (if i > 0 { sep.length() } else { 0 })
b.items(size)
out.write_string(part)
}
Text(out.to_string())
}
"split" => {
arity(args, 2, 2)
let s = string_value(args[0])
let sep = string_or_number(args[1])
let parts = if sep == "" {
s.to_array().map(c => Text(c.to_string()))
} else {
s.split(sep[:]).map(x => Text(x.to_owned())).collect()
}
b.items(parts.length())
Sequence(parts)
}
"typeof" => {
arity(args, 1, 1)
Text(
match args[0] {
Nil | Omit => "null"
Boolean(_) => "boolean"
Numeric(_) => "number"
Text(_) => "string"
Sequence(_) => "array"
Record(_) => "object"
Function(_) => "function"
},
)
}
"defined" => {
arity(args, 1, 1)
Boolean(env.contains(string_value(args[0])))
}
"fromNow" => {
arity(args, 1, 2)
let reference = if args.length() == 2 {
string_value(args[1])
} else {
string_value(env.get("now").unwrap_or(Nil))
}
Text(relative_time(string_value(args[0]), reference, b))
}
_ => raise Invalid("unknown builtin: " + name)
}
}
///|
/// Evaluate an expression with strict JSON-e types and explicit resource limits.
/// The default clock is deterministic; pass now or a context now value for real time.
pub fn evaluate_expression(
source : String,
context : Json,
now~ : String = "1970-01-01T00:00:00.000Z",
max_steps~ : Int32 = 1000000,
max_depth~ : Int32 = 128,
max_items~ : Int32 = 100000,
) -> Json raise TemplateError {
if max_steps < 1 || max_depth < 1 || max_depth > 256 || max_items < 1 {
raise Invalid("invalid resource limits")
}
let b = { remaining: max_steps, max_depth, max_items, }
let env = environment(context, b, now)
to_json(eval_source(source, env, b), b, 0)
}
///|
fn string_or_number(v : Value) -> String raise TemplateError {
match v {
Text(_) | Numeric(_) => text(v)
_ => raise Invalid("expected string or number")
}
}