use super::*;
#[cfg(test)]
mod tests {
use super::*;
use crate::cache::StringLoader;
use crate::core::environment::lambda_model;
use crate::template::{Configuration, DynValue, ObjectWrapper, SimpleObjectWrapper};
use indexmap::IndexMap;
use std::rc::Rc;
use std::sync::Arc;
fn eval_out(root: DynValue, src: &str) -> Result<String> {
let mut c = Configuration::new();
let loader = Arc::new(StringLoader::default());
c.template_loader = loader.clone();
loader.put("t.ftl", &format!("${{{src}}}"));
let t = c.get_template("t.ftl")?;
let root_model = SimpleObjectWrapper
.wrap(&root)?
.unwrap_or_else(TModel::nothing);
let mut out = Vec::new();
t.process(root_model, &mut out)?;
Ok(String::from_utf8(out).unwrap())
}
fn eval_bool(root: DynValue, src: &str) -> bool {
eval_out(root, &format!("({src})?c"))
.unwrap()
.parse()
.unwrap()
}
fn no_root() -> DynValue {
DynValue::Map(vec![])
}
#[test]
fn legacy_escaping_banned_when_autoescaping_on() {
use crate::core::{AutoEscaping, OutputFormatKind};
let mut c = Configuration::new();
c.settings.auto_escaping = AutoEscaping::On;
c.settings.output_format = OutputFormatKind::Html;
let loader = Arc::new(StringLoader::default());
c.template_loader = loader.clone();
loader.put("t.ftl", "${s?html}");
let t = c.get_template("t.ftl").unwrap();
let mut root = IndexMap::new();
root.insert("s".to_string(), TModel::from_scalar("<b>".to_string()));
let mut out = Vec::new();
let err = t.process(TModel::from_hash(root), &mut out).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("Using ?html (legacy escaping) is not allowed when auto-escaping is on with a markup output format (HTML), to avoid double-escaping mistakes."),
"{msg}"
);
}
#[test]
fn legacy_escaping_allowed_when_autoescaping_off() {
use crate::core::{AutoEscaping, OutputFormatKind};
let mut c = Configuration::new();
c.settings.auto_escaping = AutoEscaping::Off;
c.settings.output_format = OutputFormatKind::Html;
let loader = Arc::new(StringLoader::default());
c.template_loader = loader.clone();
loader.put("t.ftl", "${s?html}");
let t = c.get_template("t.ftl").unwrap();
let mut root = IndexMap::new();
root.insert("s".to_string(), TModel::from_scalar("<b>".to_string()));
let mut out = Vec::new();
t.process(TModel::from_hash(root), &mut out).unwrap();
assert_eq!(out, b"<b>");
}
#[test]
fn literals() {
assert_eq!(eval_out(no_root(), "42").unwrap(), "42");
assert_eq!(eval_out(no_root(), "\"hi\"").unwrap(), "hi");
assert_eq!(eval_out(no_root(), "true?c").unwrap(), "true");
assert_eq!(eval_out(no_root(), "1.5").unwrap(), "1.5");
}
#[test]
fn arithmetic_precedence() {
assert_eq!(eval_out(no_root(), "1 + 2 * 3").unwrap(), "7");
assert_eq!(eval_out(no_root(), "(1 + 2) * 3").unwrap(), "9");
assert_eq!(eval_out(no_root(), "10 / 4").unwrap(), "2.5");
assert_eq!(eval_out(no_root(), "1 / 3").unwrap(), "0.333");
assert_eq!(eval_out(no_root(), "10 % 3").unwrap(), "1");
assert_eq!(eval_out(no_root(), "-5 + 2").unwrap(), "-3");
}
#[test]
fn string_concat_and_interp() {
let root = DynValue::Map(vec![("x".into(), DynValue::Int(3))]);
assert_eq!(eval_out(root.clone(), "\"a\" + x + \"b\"").unwrap(), "a3b");
assert_eq!(eval_out(root.clone(), "\"v=\" + true?c").unwrap(), "v=true");
let err = eval_out(root.clone(), "\"v=\" + true").unwrap_err();
assert!(err.to_string().contains("Can't convert boolean"), "{err}");
assert_eq!(eval_out(root.clone(), "\"x=${x}\"").unwrap(), "x=3");
}
#[test]
fn comparisons() {
assert!(eval_bool(no_root(), "1 == 1"));
assert!(eval_bool(no_root(), "1 == 1.0"));
assert!(eval_bool(no_root(), "1 < 2"));
assert!(!eval_bool(no_root(), "2 >= 3"));
assert!(eval_bool(no_root(), "\"a\" == \"a\""));
assert!(eval_bool(no_root(), "\"a\" != \"b\""));
let err = eval_out(no_root(), "1 == \"a\"").unwrap_err();
assert!(
err.to_string()
.contains("Can't compare values of these types"),
"{err}"
);
let err = eval_out(no_root(), "\"a\" > \"b\"").unwrap_err();
assert!(
err.to_string()
.contains("Found string literal: \"a\". Expecting: number"),
"{err}"
);
let err = eval_out(
DynValue::Map(vec![
("x".into(), DynValue::Str("a".into())),
("y".into(), DynValue::Str("b".into())),
]),
"x > y",
)
.unwrap_err();
assert!(err.to_string().contains("Can't use operator"), "{err}");
}
#[test]
fn boolean_short_circuit() {
let root = DynValue::Map(vec![("x".into(), DynValue::Int(1))]);
assert!(!eval_bool(root.clone(), "false && (missing?boolean)"));
assert!(eval_bool(root.clone(), "true || (missing?boolean)"));
let err = eval_out(root.clone(), "true && missing").unwrap_err();
assert!(
matches!(err, TemplateError::InvalidReference { .. }),
"{err}"
);
}
#[test]
fn default_to_is_lazy() {
let root = DynValue::Map(vec![("x".into(), DynValue::Int(1))]);
assert_eq!(eval_out(root.clone(), "x!missing").unwrap(), "1");
assert_eq!(eval_out(root.clone(), "nope!\"d\"").unwrap(), "d");
assert_eq!(eval_out(root.clone(), "nope!").unwrap(), "");
}
#[test]
fn exists_builtin() {
let root = DynValue::Map(vec![("x".into(), DynValue::Int(1))]);
assert!(eval_bool(root.clone(), "x??"));
assert!(!eval_bool(root.clone(), "nope??"));
assert!(eval_bool(root.clone(), "x?exists"));
assert!(!eval_bool(root.clone(), "nope?exists"));
assert_eq!(
eval_out(root.clone(), "nope?if_exists?is_nothing?c").unwrap(),
"true"
);
assert_eq!(eval_out(root.clone(), "x?default(\"d\")").unwrap(), "1");
assert_eq!(eval_out(root.clone(), "nope?default(\"d\")").unwrap(), "d");
}
#[test]
fn dot_and_dyn_key() {
let root = DynValue::Map(vec![(
"h".into(),
DynValue::Map(vec![("k".into(), DynValue::Str("v".into()))]),
)]);
assert_eq!(eval_out(root.clone(), "h.k").unwrap(), "v");
assert_eq!(eval_out(root.clone(), "h[\"k\"]").unwrap(), "v");
let list_root = DynValue::Map(vec![(
"l".into(),
DynValue::List(vec![DynValue::Str("a".into()), DynValue::Str("b".into())]),
)]);
assert_eq!(eval_out(list_root.clone(), "l[1]").unwrap(), "b");
let err = eval_out(root.clone(), "h.missing").unwrap_err();
assert!(
matches!(err, TemplateError::InvalidReference { .. }),
"{err}"
);
}
#[test]
fn method_call() {
let m = TModel::from_method(MethodStub);
let mut root_map = IndexMap::new();
root_map.insert("f".to_string(), m);
let root = TModel::from_hash(root_map);
let mut c = Configuration::new();
let loader = Arc::new(StringLoader::default());
c.template_loader = loader.clone();
loader.put("t.ftl", "${f(\"x\")}");
let t = c.get_template("t.ftl").unwrap();
let mut out = Vec::new();
t.process(root, &mut out).unwrap();
assert_eq!(String::from_utf8(out).unwrap(), "x");
}
#[test]
fn unknown_builtin_errors() {
let root = DynValue::Map(vec![("x".into(), DynValue::Int(1))]);
let err = eval_out(root.clone(), "x?definitely_not_a_builtin").unwrap_err();
assert!(
err.to_string()
.contains("Unknown built-in: \"definitely_not_a_builtin\""),
"{err}"
);
let err = eval_out(root.clone(), "x?definitely_not_a_builtin").unwrap_err();
assert!(
!err.to_string().contains("?definitely_not_a_builtin"),
"{err}"
);
}
#[test]
fn filled_builtins() {
let root = DynValue::Map(vec![
("s".into(), DynValue::Str("a<b&c".into())),
("n".into(), DynValue::Int(5)),
(
"d".into(),
DynValue::Map(vec![("k".into(), DynValue::Int(1))]),
),
]);
assert_eq!(
eval_out(root.clone(), "s?web_safe").unwrap(),
"a<b&c"
);
assert_eq!(
eval_out(root.clone(), "s?web_safe").unwrap(),
eval_out(root.clone(), "s?html").unwrap()
);
assert_eq!(
eval_out(root.clone(), "n?is_date_like?string('yes','no')").unwrap(),
"no"
);
assert_eq!(
eval_out(root.clone(), "n?is_date?string('yes','no')").unwrap(),
eval_out(root.clone(), "n?is_date_like?string('yes','no')").unwrap()
);
assert_eq!(
eval_out(
root.clone(),
"'{\"a\": 1, \"b\": [true, null]}'?eval_json.a"
)
.unwrap(),
"1"
);
let err = eval_out(root.clone(), "'{bad'?eval_json").unwrap_err();
assert!(
err.to_string()
.contains("Failed to \"?eval_json\" string with this error"),
"{err}"
);
assert!(err.to_string().contains("---begin-message---"), "{err}");
}
#[test]
fn utf16_length_semantics() {
let root = DynValue::Map(vec![("s".into(), DynValue::Str("a\u{10000}b".into()))]);
assert_eq!(eval_out(root, "s?length").unwrap(), "4");
assert_eq!(
eval_out(DynValue::Map(vec![]), "\"abc\"?length").unwrap(),
"3"
);
}
#[test]
fn builtins_basic_set() {
let root = DynValue::Map(vec![
("s".into(), DynValue::Str("Hello World".into())),
("n".into(), DynValue::Float(1.5)),
("b".into(), DynValue::Bool(true)),
(
"seq".into(),
DynValue::List(vec![DynValue::Str("a".into()), DynValue::Str("b".into())]),
),
]);
assert_eq!(
eval_out(root.clone(), "s?upper_case").unwrap(),
"HELLO WORLD"
);
assert_eq!(
eval_out(root.clone(), "s?lower_case").unwrap(),
"hello world"
);
assert_eq!(
eval_out(root.clone(), "s?cap_first").unwrap(),
"Hello World"
);
assert_eq!(eval_out(root.clone(), "s?trim").unwrap(), "Hello World");
assert_eq!(eval_out(root.clone(), "s?length").unwrap(), "11");
assert_eq!(eval_out(root.clone(), "n?c").unwrap(), "1.5");
assert_eq!(eval_out(root.clone(), "b?c").unwrap(), "true");
assert_eq!(eval_out(root.clone(), "n?int").unwrap(), "1");
assert_eq!(eval_out(root.clone(), "n?double").unwrap(), "1.5");
assert_eq!(eval_out(root.clone(), "\"123\"?number").unwrap(), "123");
assert_eq!(
eval_out(root.clone(), "(\"true\"?boolean)?c").unwrap(),
"true"
);
assert_eq!(eval_out(root.clone(), "seq?size").unwrap(), "2");
assert_eq!(eval_out(root.clone(), "seq?first").unwrap(), "a");
assert_eq!(eval_out(root.clone(), "seq?last").unwrap(), "b");
assert_eq!(eval_out(root.clone(), "seq?join(\"-\")").unwrap(), "a-b");
assert_eq!(
eval_out(root.clone(), "seq?reverse?join(\"\")").unwrap(),
"ba"
);
assert_eq!(
eval_out(root.clone(), "seq?seq_contains(\"b\")?c").unwrap(),
"true"
);
assert_eq!(
eval_out(root.clone(), "seq?seq_contains(\"z\")?c").unwrap(),
"false"
);
assert_eq!(
eval_out(root.clone(), "s?contains(\"World\")?c").unwrap(),
"true"
);
assert_eq!(
eval_out(root.clone(), "s?starts_with(\"Hello\")?c").unwrap(),
"true"
);
assert_eq!(
eval_out(root.clone(), "s?ends_with(\"World\")?c").unwrap(),
"true"
);
assert_eq!(
eval_out(root.clone(), "s?index_of(\"World\")").unwrap(),
"6"
);
assert_eq!(eval_out(root.clone(), "s?substring(6)").unwrap(), "World");
assert_eq!(
eval_out(root.clone(), "s?matches(\"Hello.*\")?c").unwrap(),
"true"
);
assert_eq!(
eval_out(root.clone(), "s?replace(\"World\", \"Rust\")").unwrap(),
"Hello Rust"
);
assert_eq!(
eval_out(root.clone(), "\"a,b,c\"?split(\",\")?join(\"|\")").unwrap(),
"a|b|c"
);
assert_eq!(eval_out(root.clone(), "s?is_string?c").unwrap(), "true");
assert_eq!(eval_out(root.clone(), "s?is_number?c").unwrap(), "false");
assert_eq!(eval_out(root.clone(), "seq?is_sequence?c").unwrap(), "true");
assert_eq!(
eval_out(root.clone(), "seq?is_enumerable?c").unwrap(),
"true"
);
assert_eq!(
eval_out(root.clone(), "missing?is_string?c").unwrap(),
"false"
);
}
#[test]
fn ranges() {
let root = no_root();
assert_eq!(eval_out(root.clone(), "(1..5)?size").unwrap(), "5");
assert_eq!(
eval_out(root.clone(), "(1..5)?join(\",\")").unwrap(),
"1,2,3,4,5"
);
assert_eq!(
eval_out(root.clone(), "(1..<5)?join(\",\")").unwrap(),
"1,2,3,4"
);
assert_eq!(
eval_out(root.clone(), "(5..1)?join(\",\")").unwrap(),
"5,4,3,2,1"
);
assert_eq!(
eval_out(root.clone(), "(10..*3)?join(\",\")").unwrap(),
"10,11,12"
);
let out = eval_out(root.clone(), "(1..)?size").unwrap();
assert_eq!(out.replace(',', ""), "2147483647");
}
#[test]
fn add_concat_sequence_hash_semantics() {
let root = DynValue::Map(vec![]);
let src = "<#assign x = [11]><#assign x += [22]>${x[0]}|${x[1]}|${x?size}";
assert_eq!(render_template(root.clone(), src).unwrap(), "11|22|2");
let src = "<#assign x = {'a': 11}><#assign x += {'b': 22}>${x.a}|${x.b}|${x?size}";
assert_eq!(render_template(root.clone(), src).unwrap(), "11|22|2");
let src = "<#assign x = {'a': 1}><#assign x += {'a': 2}>${x.a}";
assert_eq!(render_template(root.clone(), src).unwrap(), "2");
let src = "<#assign x = 1><#assign x += 'a'>${x}";
assert_eq!(render_template(root.clone(), src).unwrap(), "1a");
let src = "<#assign x = 'a'><#assign x += 1>${x}";
assert_eq!(render_template(root.clone(), src).unwrap(), "a1");
}
#[test]
fn assign_compound_operator_errors() {
let root = DynValue::Map(vec![]);
let err = render_template(root.clone(), "<#assign foo = 'a'><#assign foo -= 1>")
.unwrap_err()
.to_string();
assert!(
err.contains("Expected a number, but assignment target variable \"foo\" has evaluated to a string."),
"{err}"
);
let err = render_template(root.clone(), "<#assign noSuchVar -= 1>")
.unwrap_err()
.to_string();
assert!(
err.contains(
"The target variable of the assignment, \"noSuchVar\", was null or missing in the template namespace, and the \"-=\" operator must get its value from there before assigning to it."
),
"{err}"
);
let err = render_template(root.clone(), "<#assign x = 1><#assign x -= 'a'>")
.unwrap_err()
.to_string();
assert!(
err.contains(
"For \"#assign\" assignment source: Expected a number, but this has evaluated to a string: ==> 'a'"
),
"{err}"
);
let err = render_template(root.clone(), "<#assign $noSuchVar += 1>")
.unwrap_err()
.to_string();
assert!(
err.contains(
"Tip: Variable references must not start with \"$\", unless the \"$\" is really part of the variable name."
),
"{err}"
);
}
fn render_template(root: DynValue, src: &str) -> Result<String> {
let mut c = Configuration::new();
let loader = Arc::new(StringLoader::default());
c.template_loader = loader.clone();
loader.put("t.ftl", src);
let t = c.get_template("t.ftl")?;
let root_model = SimpleObjectWrapper
.wrap(&root)?
.unwrap_or_else(TModel::nothing);
let mut out = Vec::new();
t.process(root_model, &mut out)?;
Ok(String::from_utf8(out).unwrap())
}
#[test]
fn substring_java_error_semantics() {
let root = DynValue::Map(vec![]);
let err = eval_out(root.clone(), "'ab'?substring(-1)")
.unwrap_err()
.to_string();
assert!(
err.contains("The index must be at least 0, but was -1."),
"{err}"
);
let err = eval_out(root.clone(), "'ab'?substring(3)")
.unwrap_err()
.to_string();
assert!(
err.contains(
"The index mustn't be greater than the length of the string, 2, but it was 3."
),
"{err}"
);
let err = eval_out(root.clone(), "'ab'?substring(1, -1)")
.unwrap_err()
.to_string();
assert!(err.contains("at least 0"), "{err}");
let err = eval_out(root.clone(), "'ab'?substring(0, 3)")
.unwrap_err()
.to_string();
assert!(
err.contains("greater than the length of the string"),
"{err}"
);
let err = eval_out(root.clone(), "'ab'?substring(1, 0)")
.unwrap_err()
.to_string();
assert!(
err.contains(
"The begin index argument, 1, shouldn't be greater than the end index argument, 0."
),
"{err}"
);
assert_eq!(eval_out(root.clone(), "'ab'?substring(1)").unwrap(), "b");
assert_eq!(
eval_out(root.clone(), "'ab'?substring(0, 2)").unwrap(),
"ab"
);
assert_eq!(eval_out(root.clone(), "'ab'?substring(2)").unwrap(), "");
let err = eval_out(root.clone(), "'ab'?substring()")
.unwrap_err()
.to_string();
assert!(
err.contains("?substring(...) expects 1 or 2 arguments but has received none."),
"{err}"
);
}
#[test]
fn is_macro_and_lambda_slots() {
let root = DynValue::Map(vec![]);
assert_eq!(eval_out(root, "true?is_macro?c").unwrap(), "false");
let lam = lambda_model(
vec!["x".to_string()],
Rc::new(Expr::new(
ExprKind::Add(
Box::new(Expr::new(
ExprKind::Ident("x".into()),
crate::span::Span::default(),
)),
Box::new(Expr::new(
ExprKind::Num(crate::value::TNumber::Int(1)),
crate::span::Span::default(),
)),
),
crate::span::Span::default(),
)),
);
assert!(lam.is_lambda());
assert_eq!(lam.type_name, "lambda");
assert!(lam
.internal::<crate::core::environment::LambdaValue>()
.is_some());
}
struct MethodStub;
impl crate::template::TemplateMethodModelEx for MethodStub {
fn exec(&self, _env: &mut crate::core::Environment, args: Vec<TModel>) -> Result<TModel> {
args.first()
.cloned()
.ok_or_else(|| TemplateError::misc("no arg"))
}
}
}
#[cfg(test)]
mod range_slice_tests {
use super::*;
use crate::cache::StringLoader;
use crate::template::{Configuration, DynValue, ObjectWrapper, SimpleObjectWrapper};
use std::sync::Arc;
fn eval_out(root: DynValue, src: &str) -> Result<String> {
render_with(root, &format!("${{{src}}}"))
}
fn render_with(root: DynValue, src: &str) -> Result<String> {
let mut c = Configuration::new();
let loader = Arc::new(StringLoader::default());
c.template_loader = loader.clone();
loader.put("t.ftl", src);
let t = c.get_template("t.ftl")?;
let root_model = SimpleObjectWrapper
.wrap(&root)?
.unwrap_or_else(TModel::nothing);
let mut out = Vec::new();
t.process(root_model, &mut out)?;
Ok(String::from_utf8(out).unwrap())
}
fn root_s() -> DynValue {
DynValue::Map(vec![
("s".to_string(), DynValue::Str("012".to_string())),
(
"seq".to_string(),
DynValue::List(vec![
DynValue::Str("a".to_string()),
DynValue::Str("b".to_string()),
DynValue::Str("c".to_string()),
]),
),
])
}
#[test]
fn eval_builtin_parses_and_evaluates() {
assert_eq!(eval_out(root_s(), "'[1,2][1]'?eval").unwrap(), "2");
assert_eq!(eval_out(root_s(), "'1+2*3'?eval").unwrap(), "7");
let err = eval_out(root_s(), "'s[5..]'?eval").unwrap_err().to_string();
assert!(err.contains("Failed to \"?eval\""), "{err}");
}
#[test]
fn range_slice_string_semantics() {
assert_eq!(
render_with(root_s(), "${s[0..*-2]}").unwrap(),
"0",
"0..*-2 → [0,-1] 裁剪为 [0]"
);
assert_eq!(render_with(root_s(), "${s[2..*2]}").unwrap(), "2");
assert_eq!(render_with(root_s(), "${s[2..]}").unwrap(), "2");
assert_eq!(render_with(root_s(), "${s[3..]}").unwrap(), "");
let err = render_with(root_s(), "${s[1..*-2]}")
.unwrap_err()
.to_string();
assert!(
err.contains("Decreasing ranges aren't allowed for slicing strings"),
"{err}"
);
let err = render_with(root_s(), "${s[4..]}").unwrap_err().to_string();
assert!(err.contains("is out of bounds"), "{err}");
let err = render_with(root_s(), "${s[-1..]}").unwrap_err().to_string();
assert!(err.contains("Negative range start index"), "{err}");
}
#[test]
fn range_slice_sequence_semantics() {
assert_eq!(
render_with(root_s(), "${seq[1..*-2]?join('')}").unwrap(),
"ba",
"seq[1..*-2] → [1,0]"
);
assert_eq!(
render_with(root_s(), "${seq[0..*-2]?join('')}").unwrap(),
"a",
"seq[0..*-2] → [0,-1] 裁剪为 [0]"
);
assert_eq!(
render_with(root_s(), "${seq[2..]?join('')}").unwrap(),
"c",
"seq[2..] → 无界"
);
assert_eq!(
render_with(root_s(), "${seq[3..]?join('')}").unwrap(),
"",
"seq[3..] → 空(自适应递增起始可 == 长度)"
);
let err = render_with(root_s(), "${seq[4..]?join('')}")
.unwrap_err()
.to_string();
assert!(err.contains("is out of bounds"), "{err}");
}
#[test]
fn interpret_transform_flow() {
let src = "<#global x=['a','b','c']><#transform r'<#foreach y in x>${y}</#foreach>'?interpret>def</#transform>";
assert_eq!(render_with(root_s(), src).unwrap(), "abcdef");
let src2 = "<#assign t = r'<#macro m>M</#macro>'?interpret><@t /><@m/>";
assert_eq!(render_with(root_s(), src2).unwrap(), "M");
}
#[test]
fn string_slicing_legacy_bug_emulation() {
assert_eq!(
render_with(root_s(), "${s[1..0]}").unwrap(),
"",
"s[1..0] → 旧版 bug 模拟为空串"
);
assert_eq!(render_with(root_s(), "${s[2..1]}").unwrap(), "");
let err = render_with(root_s(), "${s[2..0]}").unwrap_err().to_string();
assert!(
err.contains("Decreasing ranges aren't allowed for slicing strings"),
"{err}"
);
let err = render_with(root_s(), "${s[2..<0]}")
.unwrap_err()
.to_string();
assert!(
err.contains("Decreasing ranges aren't allowed for slicing strings"),
"{err}"
);
let err = render_with(root_s(), "${s[2..*-2]}")
.unwrap_err()
.to_string();
assert!(
err.contains("Decreasing ranges aren't allowed for slicing strings"),
"{err}"
);
assert_eq!(
render_with(root_s(), "${s[0..*-1]}").unwrap(),
"0",
"0..*-1 → [0]"
);
}
#[test]
fn right_unbounded_range_ici_models() {
let root = SimpleObjectWrapper
.wrap(&DynValue::Map(vec![]))
.unwrap()
.unwrap_or_else(TModel::nothing);
let mut c = Configuration::new();
let loader = Arc::new(StringLoader::default());
c.template_loader = loader.clone();
c.settings.incompatible_improvements = crate::template::Version::parse("2.3.34").unwrap();
loader.put("t.ftl", "<#assign r = 4..>${r?size}|${r[0]}|${r[1000000]}");
let t = c.get_template("t.ftl").unwrap();
let mut out = Vec::new();
t.process(root.clone(), &mut out).unwrap();
assert_eq!(
String::from_utf8(out).unwrap().replace(',', ""),
"2147483647|4|1000004",
"ICI≥2.3.21:size=Integer.MAX_VALUE、get(i)=begin+i"
);
loader.put("t2.ftl", "<#assign r = 4..>${r[-1]}");
let t = c.get_template("t2.ftl").unwrap();
let mut out = Vec::new();
let err = t.process(root.clone(), &mut out).unwrap_err();
assert!(
err.to_string()
.contains("Range item index -1 is out of bounds."),
"{err}"
);
loader.put(
"t3.ftl",
"<#assign r = 4..><#list r as i><#if i == 6><#break></#if>${i},</#list>",
);
let t = c.get_template("t3.ftl").unwrap();
let mut out = Vec::new();
t.process(root.clone(), &mut out).unwrap();
assert_eq!(String::from_utf8(out).unwrap(), "4,5,");
c.settings.incompatible_improvements = crate::template::Version::parse("2.3.20").unwrap();
loader.put(
"t4.ftl",
"<#assign r = 4..>${r?size}<#list r as i>${i}</#list>",
);
let t = c.get_template("t4.ftl").unwrap();
let mut out = Vec::new();
t.process(root.clone(), &mut out).unwrap();
assert_eq!(
String::from_utf8(out).unwrap(),
"0",
"ICI<2.3.21:size=0、迭代为空"
);
loader.put("t5.ftl", "<#assign r = 4..>${r[0]}");
let t = c.get_template("t5.ftl").unwrap();
let mut out = Vec::new();
let err = t.process(root, &mut out).unwrap_err();
assert!(
err.to_string().contains("has evaluated to null or missing"),
"{err}"
);
}
}
#[cfg(test)]
mod join_builtin_tests {
use super::*;
use crate::cache::StringLoader;
use crate::template::{Configuration, DynValue, ObjectWrapper, SimpleObjectWrapper};
use std::sync::Arc;
fn render_with(root: DynValue, src: &str) -> Result<String> {
let mut c = Configuration::new();
let loader = Arc::new(StringLoader::default());
c.template_loader = loader.clone();
loader.put("t.ftl", src);
let t = c.get_template("t.ftl")?;
let root_model = SimpleObjectWrapper
.wrap(&root)?
.unwrap_or_else(TModel::nothing);
let mut out = Vec::new();
t.process(root_model, &mut out)?;
Ok(String::from_utf8(out).unwrap())
}
fn root() -> DynValue {
DynValue::Map(vec![
(
"withNull".to_string(),
DynValue::List(vec![
DynValue::Str("a".to_string()),
DynValue::Null,
DynValue::Str("b".to_string()),
]),
),
("h".to_string(), DynValue::Map(vec![])),
(
"seq".to_string(),
DynValue::List(vec![
DynValue::Str("x".to_string()),
DynValue::Str("y".to_string()),
DynValue::Str("z".to_string()),
]),
),
("empty".to_string(), DynValue::List(vec![])),
])
}
#[test]
fn join_null_skip_and_3args() {
assert_eq!(
render_with(root(), "${withNull?join('-')}").unwrap(),
"a-b",
"null 项跳过且分隔符逻辑不变"
);
assert_eq!(
render_with(root(), "${withNull?join('-', 'EMPTY', 'LAST')}").unwrap(),
"a-bLAST",
"hadItem → afterLast"
);
assert_eq!(
render_with(root(), "${empty?join('-', 'EMPTY', 'LAST')}").unwrap(),
"EMPTY",
"空列表 → whenEmpty"
);
assert_eq!(render_with(root(), "${empty?join('-')}").unwrap(), "");
}
#[test]
fn join_lazy_collection_and_unbounded_guard() {
assert_eq!(
render_with(root(), "${(seq?map(x -> x))?join(',')}").unwrap(),
"x,y,z",
"TemplateCollectionModel 优先惰性迭代"
);
let err = render_with(root(), "${(1..)?join(',')}")
.unwrap_err()
.to_string();
assert!(
err.contains(
"The input sequence is a right-unbounded numerical range, thus, it's infinitely long, and can't processed with this built-in."
),
"{err}"
);
}
#[test]
fn join_error_wrapping_and_arg_count() {
let err = render_with(root(), "${[h]?join(',')}")
.unwrap_err()
.to_string();
assert!(
err.contains("\"?join\" failed at index 0 with this error:"),
"{err}"
);
let err = render_with(root(), "${seq?join()}")
.unwrap_err()
.to_string();
assert!(
err.contains("?join(...) expects 1 to 3 arguments but has received none."),
"{err}"
);
let err = render_with(root(), "${seq?join(',', 'a', 'b', 'c')}")
.unwrap_err()
.to_string();
assert!(
err.contains("?join(...) expects 1 to 3 arguments but has received 4."),
"{err}"
);
}
#[test]
fn var_layers_full_flow() {
let mut c = Configuration::new();
c.set_shared_variable("y", TModel::from_number(TNumber::from_i64(7)));
let loader = Arc::new(StringLoader::default());
c.template_loader = loader.clone();
loader.put(
"var-layers.ftl",
"<#import \"varlayers_lib.ftl\" as lib>\n<@foo 1/>\n${x} = ${.data_model.x} = ${.globals.x}\n<#assign x = 5>\n${x} = ${.main.x} = ${.namespace.x}\n<#global x = 6>\n${.globals.x} but ${.data_model.x} = 4\n${y} = ${.globals.y} = ${.data_model.y?default(\"ERROR\")}\n<#macro foo x>\n ${x} = ${.locals.x}\n <#local x = 2>\n ${x} = ${.locals.x}\n <#local y = 3>\n ${y} = ${.locals.y}\n</#macro>\n--\n<@lib.foo/>\n--\n",
);
loader.put(
"varlayers_lib.ftl",
"<#assign x1 = .data_model.x>\n<#assign x2 = x>\n<#assign z2 = z>\n<#macro foo>\n<@.main.foo 1/>\n ${z} = ${z2} = ${x1} = ${.data_model.x}\n 5\n ${x} == ${.globals.x}\n ${y} == ${.globals.y} == ${.data_model.y?default(\"ERROR\")}\n</#macro>\n",
);
let root = DynValue::Map(vec![
("x".into(), DynValue::Int(4)),
("z".into(), DynValue::Int(4)),
]);
let t = c.get_template("var-layers.ftl").unwrap();
let root_model = SimpleObjectWrapper
.wrap(&root)
.unwrap()
.unwrap_or_else(TModel::nothing);
let mut out = Vec::new();
t.process(root_model, &mut out).unwrap();
assert_eq!(
String::from_utf8(out).unwrap(),
concat!(
" 1 = 1\n",
" 2 = 2\n",
" 3 = 3\n",
"4 = 4 = 4\n",
"5 = 5 = 5\n",
"6 but 4 = 4\n",
"7 = 7 = 7\n",
"--\n",
" 1 = 1\n",
" 2 = 2\n",
" 3 = 3\n",
" 4 = 4 = 4 = 4\n",
" 5\n",
" 6 == 6\n",
" 7 == 7 == 7\n",
"--\n",
)
);
}
}