1use std::cmp::Ordering;
16use std::collections::HashMap;
17use std::io;
18use std::iter;
19
20use bstr::BString;
21use bstr::ByteSlice as _;
22use itertools::Itertools as _;
23use jj_lib::backend::Signature;
24use jj_lib::backend::Timestamp;
25use jj_lib::config::ConfigGetResultExt as _;
26use jj_lib::config::ConfigNamePathBuf;
27use jj_lib::config::ConfigValue;
28use jj_lib::content_hash::blake2b_hash;
29use jj_lib::hex_util;
30use jj_lib::op_store::TimestampRange;
31use jj_lib::settings::UserSettings;
32use jj_lib::str_util::StringPattern;
33use jj_lib::time_util::DatePattern;
34use serde::Deserialize;
35use serde::de::IntoDeserializer as _;
36
37use crate::config;
38use crate::formatter::FormatRecorder;
39use crate::formatter::Formatter;
40use crate::template_parser;
41use crate::template_parser::BinaryOp;
42use crate::template_parser::ExpressionKind;
43use crate::template_parser::ExpressionNode;
44use crate::template_parser::FunctionCallNode;
45use crate::template_parser::LambdaNode;
46use crate::template_parser::TemplateAliasesMap;
47use crate::template_parser::TemplateDiagnostics;
48use crate::template_parser::TemplateParseError;
49use crate::template_parser::TemplateParseErrorKind;
50use crate::template_parser::TemplateParseResult;
51use crate::template_parser::UnaryOp;
52use crate::templater::AnyTemplateProperty;
53use crate::templater::BoxedAnyProperty;
54use crate::templater::BoxedSerializeProperty;
55use crate::templater::BoxedTemplateProperty;
56use crate::templater::CoalesceTemplate;
57use crate::templater::ConcatTemplate;
58use crate::templater::ConditionalProperty;
59use crate::templater::Email;
60use crate::templater::HyperlinkTemplate;
61use crate::templater::JoinTemplate;
62use crate::templater::LabelTemplate;
63use crate::templater::ListMapProperty;
64use crate::templater::ListPropertyTemplate;
65use crate::templater::Literal;
66use crate::templater::PlainTextFormattedProperty;
67use crate::templater::PropertyPlaceholder;
68use crate::templater::RawEscapeSequenceTemplate;
69use crate::templater::ReformatTemplate;
70use crate::templater::RegexCaptures;
71use crate::templater::SeparateTemplate;
72use crate::templater::SizeHint;
73use crate::templater::Template;
74use crate::templater::TemplateFormatter;
75use crate::templater::TemplateProperty;
76use crate::templater::TemplatePropertyError;
77use crate::templater::TemplatePropertyExt as _;
78use crate::templater::TemplateRenderer;
79use crate::templater::WrapTemplateProperty;
80use crate::text_util;
81use crate::text_util::write_replaced;
82use crate::time_util;
83
84pub trait TemplateLanguage<'a> {
90 type Property: CoreTemplatePropertyVar<'a> + 'a;
91
92 fn settings(&self) -> &UserSettings;
93
94 fn build_function(
99 &self,
100 diagnostics: &mut TemplateDiagnostics,
101 build_ctx: &BuildContext<Self::Property>,
102 function: &FunctionCallNode,
103 ) -> TemplateParseResult<Self::Property>;
104
105 fn build_method(
108 &self,
109 diagnostics: &mut TemplateDiagnostics,
110 build_ctx: &BuildContext<Self::Property>,
111 property: Self::Property,
112 function: &FunctionCallNode,
113 ) -> TemplateParseResult<Self::Property>;
114}
115
116macro_rules! impl_property_wrappers {
124 ($kind:path $(=> $var:ident)? { $($body:tt)* }) => {
125 $crate::template_builder::_impl_property_wrappers_many!(
126 [], 'static, $kind $(=> $var)?, { $($body)* });
127 };
128 (<$a:lifetime $(, $p:lifetime)* $(, $q:ident)*>
130 $kind:path $(=> $var:ident)? { $($body:tt)* }) => {
131 $crate::template_builder::_impl_property_wrappers_many!(
132 [$a, $($p,)* $($q,)*], $a, $kind $(=> $var)?, { $($body)* });
133 };
134}
135
136macro_rules! _impl_property_wrappers_many {
137 ($ps:tt, $a:lifetime, $kind:path, { $( $var:ident($ty:ty), )* }) => {
140 $(
141 $crate::template_builder::_impl_property_wrappers_one!(
142 $ps, $a, $kind, $var, $ty, std::convert::identity);
143 )*
144 };
145 ($ps:tt, $a:lifetime, $kind:path => $var:ident, { $( $ignored_var:ident($ty:ty), )* }) => {
148 $(
149 $crate::template_builder::_impl_property_wrappers_one!(
150 $ps, $a, $kind, $var, $ty, $crate::templater::WrapTemplateProperty::wrap_property);
151 )*
152 };
153}
154
155macro_rules! _impl_property_wrappers_one {
156 ([$($p:tt)*], $a:lifetime, $kind:path, $var:ident, $ty:ty, $inner:path) => {
157 impl<$($p)*> $crate::templater::WrapTemplateProperty<$a, $ty> for $kind {
158 fn wrap_property(property: $crate::templater::BoxedTemplateProperty<$a, $ty>) -> Self {
159 Self::$var($inner(property))
160 }
161 }
162 };
163}
164
165pub(crate) use _impl_property_wrappers_many;
166pub(crate) use _impl_property_wrappers_one;
167pub(crate) use impl_property_wrappers;
168
169pub trait CoreTemplatePropertyVar<'a>
171where
172 Self: WrapTemplateProperty<'a, BString>,
173 Self: WrapTemplateProperty<'a, Vec<BString>>,
174 Self: WrapTemplateProperty<'a, String>,
175 Self: WrapTemplateProperty<'a, Vec<String>>,
176 Self: WrapTemplateProperty<'a, bool>,
177 Self: WrapTemplateProperty<'a, i64>,
178 Self: WrapTemplateProperty<'a, Option<i64>>,
179 Self: WrapTemplateProperty<'a, ConfigValue>,
180 Self: WrapTemplateProperty<'a, Option<ConfigValue>>,
181 Self: WrapTemplateProperty<'a, Signature>,
182 Self: WrapTemplateProperty<'a, Email>,
183 Self: WrapTemplateProperty<'a, SizeHint>,
184 Self: WrapTemplateProperty<'a, RegexCaptures>,
185 Self: WrapTemplateProperty<'a, Timestamp>,
186 Self: WrapTemplateProperty<'a, TimestampRange>,
187{
188 fn wrap_template(template: Box<dyn Template + 'a>) -> Self;
189 fn wrap_any(property: BoxedAnyProperty<'a>) -> Self;
190 fn wrap_any_list(property: BoxedAnyProperty<'a>) -> Self;
191
192 fn type_name(&self) -> &'static str;
194
195 fn try_into_byte_string(self) -> Result<BoxedTemplateProperty<'a, BString>, Self>;
197 fn try_into_string(self) -> Result<BoxedTemplateProperty<'a, String>, Self>;
199 fn try_into_boolean(self) -> Result<BoxedTemplateProperty<'a, bool>, Self>;
201 fn try_into_integer(self) -> Result<BoxedTemplateProperty<'a, i64>, Self>;
202 fn try_into_timestamp(self) -> Result<BoxedTemplateProperty<'a, Timestamp>, Self>;
203
204 fn try_into_serialize(self) -> Option<BoxedSerializeProperty<'a>>;
205 fn try_into_template(self) -> Option<Box<dyn Template + 'a>>;
206
207 fn try_into_eq(self, other: Self) -> Option<BoxedTemplateProperty<'a, bool>>;
209
210 fn try_into_cmp(self, other: Self) -> Option<BoxedTemplateProperty<'a, Ordering>>;
212}
213
214pub enum CoreTemplatePropertyKind<'a> {
215 ByteString(BoxedTemplateProperty<'a, BString>),
216 ByteStringList(BoxedTemplateProperty<'a, Vec<BString>>),
217 String(BoxedTemplateProperty<'a, String>),
218 StringList(BoxedTemplateProperty<'a, Vec<String>>),
219 Boolean(BoxedTemplateProperty<'a, bool>),
220 Integer(BoxedTemplateProperty<'a, i64>),
221 IntegerOpt(BoxedTemplateProperty<'a, Option<i64>>),
222 ConfigValue(BoxedTemplateProperty<'a, ConfigValue>),
223 ConfigValueOpt(BoxedTemplateProperty<'a, Option<ConfigValue>>),
224 Signature(BoxedTemplateProperty<'a, Signature>),
225 Email(BoxedTemplateProperty<'a, Email>),
226 SizeHint(BoxedTemplateProperty<'a, SizeHint>),
227 RegexCaptures(BoxedTemplateProperty<'a, RegexCaptures>),
228 Timestamp(BoxedTemplateProperty<'a, Timestamp>),
229 TimestampRange(BoxedTemplateProperty<'a, TimestampRange>),
230
231 Template(Box<dyn Template + 'a>),
242 Any(BoxedAnyProperty<'a>),
243 AnyList(BoxedAnyProperty<'a>),
244}
245
246macro_rules! impl_core_property_wrappers {
251 ($($head:tt)+) => {
252 $crate::template_builder::impl_property_wrappers!($($head)+ {
253 ByteString(bstr::BString),
254 ByteStringList(Vec<bstr::BString>),
255 String(String),
256 StringList(Vec<String>),
257 Boolean(bool),
258 Integer(i64),
259 IntegerOpt(Option<i64>),
260 ConfigValue(jj_lib::config::ConfigValue),
261 ConfigValueOpt(Option<jj_lib::config::ConfigValue>),
262 Signature(jj_lib::backend::Signature),
263 Email($crate::templater::Email),
264 SizeHint($crate::templater::SizeHint),
265 RegexCaptures($crate::templater::RegexCaptures),
266 Timestamp(jj_lib::backend::Timestamp),
267 TimestampRange(jj_lib::op_store::TimestampRange),
268 });
269 };
270}
271
272pub(crate) use impl_core_property_wrappers;
273
274impl_core_property_wrappers!(<'a> CoreTemplatePropertyKind<'a>);
275
276impl<'a> CoreTemplatePropertyVar<'a> for CoreTemplatePropertyKind<'a> {
277 fn wrap_template(template: Box<dyn Template + 'a>) -> Self {
278 Self::Template(template)
279 }
280
281 fn wrap_any(property: BoxedAnyProperty<'a>) -> Self {
282 Self::Any(property)
283 }
284
285 fn wrap_any_list(property: BoxedAnyProperty<'a>) -> Self {
286 Self::AnyList(property)
287 }
288
289 fn type_name(&self) -> &'static str {
290 match self {
291 Self::ByteString(_) => "ByteString",
292 Self::ByteStringList(_) => "List<ByteString>",
293 Self::String(_) => "String",
294 Self::StringList(_) => "List<String>",
295 Self::Boolean(_) => "Boolean",
296 Self::Integer(_) => "Integer",
297 Self::IntegerOpt(_) => "Option<Integer>",
298 Self::ConfigValue(_) => "ConfigValue",
299 Self::ConfigValueOpt(_) => "Option<ConfigValue>",
300 Self::Signature(_) => "Signature",
301 Self::Email(_) => "Email",
302 Self::SizeHint(_) => "SizeHint",
303 Self::RegexCaptures(_) => "RegexCaptures",
304 Self::Timestamp(_) => "Timestamp",
305 Self::TimestampRange(_) => "TimestampRange",
306 Self::Template(_) => "Template",
307 Self::Any(_) => "Any",
308 Self::AnyList(_) => "AnyList",
309 }
310 }
311
312 fn try_into_byte_string(self) -> Result<BoxedTemplateProperty<'a, BString>, Self> {
313 match self {
314 Self::ByteString(property) => Ok(property),
315 _ => Err(self),
316 }
317 }
318
319 fn try_into_string(self) -> Result<BoxedTemplateProperty<'a, String>, Self> {
320 match self {
321 Self::String(property) => Ok(property),
322 _ => Err(self),
323 }
324 }
325
326 fn try_into_boolean(self) -> Result<BoxedTemplateProperty<'a, bool>, Self> {
327 match self {
328 Self::ByteString(property) => Ok(property.map(|s| !s.is_empty()).into_dyn()),
329 Self::ByteStringList(property) => Ok(property.map(|l| !l.is_empty()).into_dyn()),
330 Self::String(property) => Ok(property.map(|s| !s.is_empty()).into_dyn()),
331 Self::StringList(property) => Ok(property.map(|l| !l.is_empty()).into_dyn()),
332 Self::Boolean(property) => Ok(property),
333 Self::Integer(_) => Err(self),
334 Self::IntegerOpt(property) => Ok(property.map(|opt| opt.is_some()).into_dyn()),
335 Self::ConfigValue(_) => Err(self),
336 Self::ConfigValueOpt(property) => Ok(property.map(|opt| opt.is_some()).into_dyn()),
337 Self::Signature(_) => Err(self),
338 Self::Email(property) => Ok(property.map(|e| !e.0.is_empty()).into_dyn()),
339 Self::SizeHint(_) => Err(self),
340 Self::RegexCaptures(_) => Err(self),
341 Self::Timestamp(_) => Err(self),
342 Self::TimestampRange(_) => Err(self),
343 Self::Template(_) => Err(self),
347 Self::Any(_) => Err(self),
348 Self::AnyList(_) => Err(self),
349 }
350 }
351
352 fn try_into_integer(self) -> Result<BoxedTemplateProperty<'a, i64>, Self> {
353 match self {
354 Self::Integer(property) => Ok(property),
355 Self::IntegerOpt(property) => Ok(property.try_unwrap("Integer").into_dyn()),
356 _ => Err(self),
357 }
358 }
359
360 fn try_into_timestamp(self) -> Result<BoxedTemplateProperty<'a, Timestamp>, Self> {
361 match self {
362 Self::Timestamp(property) => Ok(property),
363 _ => Err(self),
364 }
365 }
366
367 fn try_into_serialize(self) -> Option<BoxedSerializeProperty<'a>> {
368 match self {
369 Self::ByteString(property) => Some(property.into_serialize()),
370 Self::ByteStringList(property) => Some(property.into_serialize()),
371 Self::String(property) => Some(property.into_serialize()),
372 Self::StringList(property) => Some(property.into_serialize()),
373 Self::Boolean(property) => Some(property.into_serialize()),
374 Self::Integer(property) => Some(property.into_serialize()),
375 Self::IntegerOpt(property) => Some(property.into_serialize()),
376 Self::ConfigValue(property) => {
377 Some(property.map(config::to_serializable_value).into_serialize())
378 }
379 Self::ConfigValueOpt(property) => Some(
380 property
381 .map(|opt| opt.map(config::to_serializable_value))
382 .into_serialize(),
383 ),
384 Self::Signature(property) => Some(property.into_serialize()),
385 Self::Email(property) => Some(property.into_serialize()),
386 Self::SizeHint(property) => Some(property.into_serialize()),
387 Self::RegexCaptures(_) => None,
388 Self::Timestamp(property) => Some(property.into_serialize()),
389 Self::TimestampRange(property) => Some(property.into_serialize()),
390 Self::Template(_) => None,
391 Self::Any(property) => property.try_into_serialize(),
392 Self::AnyList(property) => property.try_into_serialize(),
393 }
394 }
395
396 fn try_into_template(self) -> Option<Box<dyn Template + 'a>> {
397 match self {
398 Self::ByteString(property) => Some(property.into_template()),
399 Self::ByteStringList(property) => Some(property.into_template()),
400 Self::String(property) => Some(property.into_template()),
401 Self::StringList(property) => Some(property.into_template()),
402 Self::Boolean(property) => Some(property.into_template()),
403 Self::Integer(property) => Some(property.into_template()),
404 Self::IntegerOpt(property) => Some(property.into_template()),
405 Self::ConfigValue(property) => Some(property.into_template()),
406 Self::ConfigValueOpt(property) => Some(property.into_template()),
407 Self::Signature(property) => Some(property.into_template()),
408 Self::Email(property) => Some(property.into_template()),
409 Self::SizeHint(_) => None,
410 Self::RegexCaptures(_) => None,
411 Self::Timestamp(property) => Some(property.into_template()),
412 Self::TimestampRange(property) => Some(property.into_template()),
413 Self::Template(template) => Some(template),
414 Self::Any(property) => property.try_into_template(),
415 Self::AnyList(property) => property.try_into_template(),
416 }
417 }
418
419 fn try_into_eq(self, other: Self) -> Option<BoxedTemplateProperty<'a, bool>> {
420 match (self, other) {
421 (Self::ByteString(lhs), Self::ByteString(rhs)) => {
422 Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
423 }
424 (Self::ByteString(lhs), Self::String(rhs)) => {
425 Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
426 }
427 (Self::String(lhs), Self::ByteString(rhs)) => {
428 Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
429 }
430 (Self::String(lhs), Self::String(rhs)) => {
431 Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
432 }
433 (Self::String(lhs), Self::Email(rhs)) => {
434 Some((lhs, rhs).map(|(l, r)| l == r.0).into_dyn())
435 }
436 (Self::Boolean(lhs), Self::Boolean(rhs)) => {
437 Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
438 }
439 (Self::Integer(lhs), Self::Integer(rhs)) => {
440 Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
441 }
442 (Self::Integer(lhs), Self::IntegerOpt(rhs)) => {
443 Some((lhs, rhs).map(|(l, r)| Some(l) == r).into_dyn())
444 }
445 (Self::IntegerOpt(lhs), Self::Integer(rhs)) => {
446 Some((lhs, rhs).map(|(l, r)| l == Some(r)).into_dyn())
447 }
448 (Self::IntegerOpt(lhs), Self::IntegerOpt(rhs)) => {
449 Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
450 }
451 (Self::Email(lhs), Self::Email(rhs)) => {
452 Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
453 }
454 (Self::Email(lhs), Self::String(rhs)) => {
455 Some((lhs, rhs).map(|(l, r)| l.0 == r).into_dyn())
456 }
457 (Self::ByteString(_), _) => None,
458 (Self::ByteStringList(_), _) => None,
459 (Self::String(_), _) => None,
460 (Self::StringList(_), _) => None,
461 (Self::Boolean(_), _) => None,
462 (Self::Integer(_), _) => None,
463 (Self::IntegerOpt(_), _) => None,
464 (Self::ConfigValue(_), _) => None,
465 (Self::ConfigValueOpt(_), _) => None,
466 (Self::Signature(_), _) => None,
467 (Self::Email(_), _) => None,
468 (Self::SizeHint(_), _) => None,
469 (Self::RegexCaptures(_), _) => None,
470 (Self::Timestamp(_), _) => None,
471 (Self::TimestampRange(_), _) => None,
472 (Self::Template(_), _) => None,
473 (Self::Any(_), _) => None,
474 (Self::AnyList(_), _) => None,
475 }
476 }
477
478 fn try_into_cmp(self, other: Self) -> Option<BoxedTemplateProperty<'a, Ordering>> {
479 match (self, other) {
480 (Self::Integer(lhs), Self::Integer(rhs)) => {
481 Some((lhs, rhs).map(|(l, r)| l.cmp(&r)).into_dyn())
482 }
483 (Self::Integer(lhs), Self::IntegerOpt(rhs)) => {
484 Some((lhs, rhs).map(|(l, r)| Some(l).cmp(&r)).into_dyn())
485 }
486 (Self::IntegerOpt(lhs), Self::Integer(rhs)) => {
487 Some((lhs, rhs).map(|(l, r)| l.cmp(&Some(r))).into_dyn())
488 }
489 (Self::IntegerOpt(lhs), Self::IntegerOpt(rhs)) => {
490 Some((lhs, rhs).map(|(l, r)| l.cmp(&r)).into_dyn())
491 }
492 (Self::ByteString(_), _) => None,
493 (Self::ByteStringList(_), _) => None,
494 (Self::String(_), _) => None,
495 (Self::StringList(_), _) => None,
496 (Self::Boolean(_), _) => None,
497 (Self::Integer(_), _) => None,
498 (Self::IntegerOpt(_), _) => None,
499 (Self::ConfigValue(_), _) => None,
500 (Self::ConfigValueOpt(_), _) => None,
501 (Self::Signature(_), _) => None,
502 (Self::Email(_), _) => None,
503 (Self::SizeHint(_), _) => None,
504 (Self::RegexCaptures(_), _) => None,
505 (Self::Timestamp(_), _) => None,
506 (Self::TimestampRange(_), _) => None,
507 (Self::Template(_), _) => None,
508 (Self::Any(_), _) => None,
509 (Self::AnyList(_), _) => None,
510 }
511 }
512}
513
514pub type TemplateBuildFunctionFn<'a, L, P> =
521 fn(&L, &mut TemplateDiagnostics, &BuildContext<P>, &FunctionCallNode) -> TemplateParseResult<P>;
522
523type BuildMethodFn<'a, L, T, P> = fn(
524 &L,
525 &mut TemplateDiagnostics,
526 &BuildContext<P>,
527 T,
528 &FunctionCallNode,
529) -> TemplateParseResult<P>;
530
531pub type TemplateBuildMethodFn<'a, L, T, P> = BuildMethodFn<'a, L, BoxedTemplateProperty<'a, T>, P>;
533
534pub type BuildTemplateMethodFn<'a, L, P> = BuildMethodFn<'a, L, Box<dyn Template + 'a>, P>;
536
537pub type BuildAnyMethodFn<'a, L, P> = BuildMethodFn<'a, L, BoxedAnyProperty<'a>, P>;
539
540pub type TemplateBuildFunctionFnMap<'a, L, P = <L as TemplateLanguage<'a>>::Property> =
542 HashMap<&'static str, TemplateBuildFunctionFn<'a, L, P>>;
543
544pub type TemplateBuildMethodFnMap<'a, L, T, P = <L as TemplateLanguage<'a>>::Property> =
546 HashMap<&'static str, TemplateBuildMethodFn<'a, L, T, P>>;
547
548pub type BuildTemplateMethodFnMap<'a, L, P = <L as TemplateLanguage<'a>>::Property> =
550 HashMap<&'static str, BuildTemplateMethodFn<'a, L, P>>;
551
552pub type BuildAnyMethodFnMap<'a, L, P = <L as TemplateLanguage<'a>>::Property> =
554 HashMap<&'static str, BuildAnyMethodFn<'a, L, P>>;
555
556pub struct CoreTemplateBuildFnTable<'a, L: ?Sized, P = <L as TemplateLanguage<'a>>::Property> {
558 pub functions: TemplateBuildFunctionFnMap<'a, L, P>,
559 pub byte_string_methods: TemplateBuildMethodFnMap<'a, L, BString, P>,
560 pub byte_string_list_methods: TemplateBuildMethodFnMap<'a, L, Vec<BString>, P>,
561 pub string_methods: TemplateBuildMethodFnMap<'a, L, String, P>,
562 pub string_list_methods: TemplateBuildMethodFnMap<'a, L, Vec<String>, P>,
563 pub boolean_methods: TemplateBuildMethodFnMap<'a, L, bool, P>,
564 pub integer_methods: TemplateBuildMethodFnMap<'a, L, i64, P>,
565 pub config_value_methods: TemplateBuildMethodFnMap<'a, L, ConfigValue, P>,
566 pub email_methods: TemplateBuildMethodFnMap<'a, L, Email, P>,
567 pub signature_methods: TemplateBuildMethodFnMap<'a, L, Signature, P>,
568 pub size_hint_methods: TemplateBuildMethodFnMap<'a, L, SizeHint, P>,
569 pub regex_captures_methods: TemplateBuildMethodFnMap<'a, L, RegexCaptures, P>,
570 pub timestamp_methods: TemplateBuildMethodFnMap<'a, L, Timestamp, P>,
571 pub timestamp_range_methods: TemplateBuildMethodFnMap<'a, L, TimestampRange, P>,
572 pub template_methods: BuildTemplateMethodFnMap<'a, L, P>,
573 pub any_methods: BuildAnyMethodFnMap<'a, L, P>,
574 pub any_list_methods: BuildAnyMethodFnMap<'a, L, P>,
575}
576
577pub fn merge_fn_map<'s, F>(base: &mut HashMap<&'s str, F>, extension: HashMap<&'s str, F>) {
578 for (name, function) in extension {
579 if base.insert(name, function).is_some() {
580 panic!("Conflicting template definitions for '{name}' function");
581 }
582 }
583}
584
585impl<L: ?Sized, P> CoreTemplateBuildFnTable<'_, L, P> {
586 pub fn empty() -> Self {
587 Self {
588 functions: HashMap::new(),
589 byte_string_methods: HashMap::new(),
590 byte_string_list_methods: HashMap::new(),
591 string_methods: HashMap::new(),
592 string_list_methods: HashMap::new(),
593 boolean_methods: HashMap::new(),
594 integer_methods: HashMap::new(),
595 config_value_methods: HashMap::new(),
596 signature_methods: HashMap::new(),
597 email_methods: HashMap::new(),
598 size_hint_methods: HashMap::new(),
599 regex_captures_methods: HashMap::new(),
600 timestamp_methods: HashMap::new(),
601 timestamp_range_methods: HashMap::new(),
602 template_methods: HashMap::new(),
603 any_methods: HashMap::new(),
604 any_list_methods: HashMap::new(),
605 }
606 }
607
608 pub fn merge(&mut self, other: Self) {
609 let Self {
610 functions,
611 byte_string_methods,
612 byte_string_list_methods,
613 string_methods,
614 string_list_methods,
615 boolean_methods,
616 integer_methods,
617 config_value_methods,
618 signature_methods,
619 email_methods,
620 size_hint_methods,
621 regex_captures_methods,
622 timestamp_methods,
623 timestamp_range_methods,
624 template_methods,
625 any_methods,
626 any_list_methods,
627 } = other;
628
629 merge_fn_map(&mut self.functions, functions);
630 merge_fn_map(&mut self.byte_string_methods, byte_string_methods);
631 merge_fn_map(&mut self.byte_string_list_methods, byte_string_list_methods);
632 merge_fn_map(&mut self.string_methods, string_methods);
633 merge_fn_map(&mut self.string_list_methods, string_list_methods);
634 merge_fn_map(&mut self.boolean_methods, boolean_methods);
635 merge_fn_map(&mut self.integer_methods, integer_methods);
636 merge_fn_map(&mut self.config_value_methods, config_value_methods);
637 merge_fn_map(&mut self.signature_methods, signature_methods);
638 merge_fn_map(&mut self.email_methods, email_methods);
639 merge_fn_map(&mut self.size_hint_methods, size_hint_methods);
640 merge_fn_map(&mut self.regex_captures_methods, regex_captures_methods);
641 merge_fn_map(&mut self.timestamp_methods, timestamp_methods);
642 merge_fn_map(&mut self.timestamp_range_methods, timestamp_range_methods);
643 merge_fn_map(&mut self.template_methods, template_methods);
644 merge_fn_map(&mut self.any_methods, any_methods);
645 merge_fn_map(&mut self.any_list_methods, any_list_methods);
646 }
647}
648
649impl<'a, L> CoreTemplateBuildFnTable<'a, L, L::Property>
650where
651 L: TemplateLanguage<'a> + ?Sized,
652{
653 pub fn builtin() -> Self {
655 Self {
656 functions: builtin_functions(),
657 byte_string_methods: builtin_byte_string_methods(),
658 byte_string_list_methods: builtin_formattable_list_methods(),
659 string_methods: builtin_string_methods(),
660 string_list_methods: builtin_formattable_list_methods(),
661 boolean_methods: HashMap::new(),
662 integer_methods: HashMap::new(),
663 config_value_methods: builtin_config_value_methods(),
664 signature_methods: builtin_signature_methods(),
665 email_methods: builtin_email_methods(),
666 size_hint_methods: builtin_size_hint_methods(),
667 regex_captures_methods: builtin_regex_captures_methods(),
668 timestamp_methods: builtin_timestamp_methods(),
669 timestamp_range_methods: builtin_timestamp_range_methods(),
670 template_methods: HashMap::new(),
671 any_methods: HashMap::new(),
672 any_list_methods: builtin_any_list_methods(),
673 }
674 }
675
676 pub fn build_function(
678 &self,
679 language: &L,
680 diagnostics: &mut TemplateDiagnostics,
681 build_ctx: &BuildContext<L::Property>,
682 function: &FunctionCallNode,
683 ) -> TemplateParseResult<L::Property> {
684 let table = &self.functions;
685 let build = template_parser::lookup_function(table, function)?;
686 build(language, diagnostics, build_ctx, function)
687 }
688
689 pub fn build_method(
692 &self,
693 language: &L,
694 diagnostics: &mut TemplateDiagnostics,
695 build_ctx: &BuildContext<L::Property>,
696 property: CoreTemplatePropertyKind<'a>,
697 function: &FunctionCallNode,
698 ) -> TemplateParseResult<L::Property> {
699 let type_name = property.type_name();
700 match property {
701 CoreTemplatePropertyKind::ByteString(property) => {
702 let table = &self.byte_string_methods;
703 let build = template_parser::lookup_method(type_name, table, function)?;
704 build(language, diagnostics, build_ctx, property, function)
705 }
706 CoreTemplatePropertyKind::ByteStringList(property) => {
707 let table = &self.byte_string_list_methods;
708 let build = template_parser::lookup_method(type_name, table, function)?;
709 build(language, diagnostics, build_ctx, property, function)
710 }
711 CoreTemplatePropertyKind::String(property) => {
712 let table = &self.string_methods;
713 let build = template_parser::lookup_method(type_name, table, function)?;
714 build(language, diagnostics, build_ctx, property, function)
715 }
716 CoreTemplatePropertyKind::StringList(property) => {
717 let table = &self.string_list_methods;
718 let build = template_parser::lookup_method(type_name, table, function)?;
719 build(language, diagnostics, build_ctx, property, function)
720 }
721 CoreTemplatePropertyKind::Boolean(property) => {
722 let table = &self.boolean_methods;
723 let build = template_parser::lookup_method(type_name, table, function)?;
724 build(language, diagnostics, build_ctx, property, function)
725 }
726 CoreTemplatePropertyKind::Integer(property) => {
727 let table = &self.integer_methods;
728 let build = template_parser::lookup_method(type_name, table, function)?;
729 build(language, diagnostics, build_ctx, property, function)
730 }
731 CoreTemplatePropertyKind::IntegerOpt(property) => {
732 let type_name = "Integer";
733 let table = &self.integer_methods;
734 let build = template_parser::lookup_method(type_name, table, function)?;
735 let inner_property = property.try_unwrap(type_name).into_dyn();
736 build(language, diagnostics, build_ctx, inner_property, function)
737 }
738 CoreTemplatePropertyKind::ConfigValue(property) => {
739 let table = &self.config_value_methods;
740 let build = template_parser::lookup_method(type_name, table, function)?;
741 build(language, diagnostics, build_ctx, property, function)
742 }
743 CoreTemplatePropertyKind::ConfigValueOpt(property) => {
744 let type_name = "ConfigValue";
745 let table = &self.config_value_methods;
746 let build = template_parser::lookup_method(type_name, table, function)?;
747 let inner_property = property.try_unwrap(type_name).into_dyn();
748 build(language, diagnostics, build_ctx, inner_property, function)
749 }
750 CoreTemplatePropertyKind::Signature(property) => {
751 let table = &self.signature_methods;
752 let build = template_parser::lookup_method(type_name, table, function)?;
753 build(language, diagnostics, build_ctx, property, function)
754 }
755 CoreTemplatePropertyKind::Email(property) => {
756 let table = &self.email_methods;
757 let build = template_parser::lookup_method(type_name, table, function)?;
758 build(language, diagnostics, build_ctx, property, function)
759 }
760 CoreTemplatePropertyKind::SizeHint(property) => {
761 let table = &self.size_hint_methods;
762 let build = template_parser::lookup_method(type_name, table, function)?;
763 build(language, diagnostics, build_ctx, property, function)
764 }
765 CoreTemplatePropertyKind::RegexCaptures(property) => {
766 let table = &self.regex_captures_methods;
767 let build = template_parser::lookup_method(type_name, table, function)?;
768 build(language, diagnostics, build_ctx, property, function)
769 }
770 CoreTemplatePropertyKind::Timestamp(property) => {
771 let table = &self.timestamp_methods;
772 let build = template_parser::lookup_method(type_name, table, function)?;
773 build(language, diagnostics, build_ctx, property, function)
774 }
775 CoreTemplatePropertyKind::TimestampRange(property) => {
776 let table = &self.timestamp_range_methods;
777 let build = template_parser::lookup_method(type_name, table, function)?;
778 build(language, diagnostics, build_ctx, property, function)
779 }
780 CoreTemplatePropertyKind::Template(template) => {
781 let table = &self.template_methods;
782 let build = template_parser::lookup_method(type_name, table, function)?;
783 build(language, diagnostics, build_ctx, template, function)
784 }
785 CoreTemplatePropertyKind::Any(property) => {
786 let table = &self.any_methods;
787 let build = template_parser::lookup_method(type_name, table, function)?;
788 build(language, diagnostics, build_ctx, property, function)
789 }
790 CoreTemplatePropertyKind::AnyList(property) => {
791 let table = &self.any_list_methods;
792 let build = template_parser::lookup_method(type_name, table, function)?;
793 build(language, diagnostics, build_ctx, property, function)
794 }
795 }
796 }
797}
798
799pub struct Expression<P> {
801 property: P,
802 labels: Vec<String>,
803}
804
805impl<P> Expression<P> {
806 fn unlabeled(property: P) -> Self {
807 let labels = vec![];
808 Self { property, labels }
809 }
810
811 fn with_label(property: P, label: impl Into<String>) -> Self {
812 let labels = vec![label.into()];
813 Self { property, labels }
814 }
815}
816
817impl<'a, P: CoreTemplatePropertyVar<'a>> Expression<P> {
818 pub fn type_name(&self) -> &'static str {
819 self.property.type_name()
820 }
821
822 pub fn try_into_boolean(self) -> Option<BoxedTemplateProperty<'a, bool>> {
823 self.property.try_into_boolean().ok()
824 }
825
826 pub fn try_into_integer(self) -> Option<BoxedTemplateProperty<'a, i64>> {
827 self.property.try_into_integer().ok()
828 }
829
830 pub fn try_into_timestamp(self) -> Option<BoxedTemplateProperty<'a, Timestamp>> {
831 self.property.try_into_timestamp().ok()
832 }
833
834 pub fn try_into_byte_stringify(self) -> Option<BoxedTemplateProperty<'a, BString>> {
837 let property = match self.property.try_into_byte_string() {
838 Ok(bytes_property) => return Some(bytes_property),
839 Err(property) => property,
840 };
841 let property = match property.try_into_string() {
842 Ok(string_property) => return Some(string_property.map(BString::from).into_dyn()),
843 Err(property) => property,
844 };
845 let template = property.try_into_template()?;
846 Some(PlainTextFormattedProperty::new(template).into_dyn())
847 }
848
849 pub fn try_into_stringify(self) -> Option<BoxedTemplateProperty<'a, String>> {
851 let from_bytes =
852 |s: BString| Ok(String::from_utf8(s.into()).map_err(|err| err.utf8_error())?);
853 let property = match self.property.try_into_string() {
854 Ok(string_property) => return Some(string_property),
855 Err(property) => property,
856 };
857 let property = match property.try_into_byte_string() {
858 Ok(bytes_property) => return Some(bytes_property.and_then(from_bytes).into_dyn()),
859 Err(property) => property,
860 };
861 let template = property.try_into_template()?;
862 Some(
863 PlainTextFormattedProperty::new(template)
864 .and_then(from_bytes)
865 .into_dyn(),
866 )
867 }
868
869 pub fn try_into_serialize(self) -> Option<BoxedSerializeProperty<'a>> {
870 self.property.try_into_serialize()
871 }
872
873 pub fn try_into_template(self) -> Option<Box<dyn Template + 'a>> {
874 let template = self.property.try_into_template()?;
875 if self.labels.is_empty() {
876 Some(template)
877 } else {
878 Some(Box::new(LabelTemplate::new(template, Literal(self.labels))))
879 }
880 }
881
882 pub fn try_into_eq(self, other: Self) -> Option<BoxedTemplateProperty<'a, bool>> {
883 self.property.try_into_eq(other.property)
884 }
885
886 pub fn try_into_cmp(self, other: Self) -> Option<BoxedTemplateProperty<'a, Ordering>> {
887 self.property.try_into_cmp(other.property)
888 }
889}
890
891impl<'a, P: CoreTemplatePropertyVar<'a>> AnyTemplateProperty<'a> for Expression<P> {
892 fn try_into_serialize(self: Box<Self>) -> Option<BoxedSerializeProperty<'a>> {
893 (*self).try_into_serialize()
894 }
895
896 fn try_into_template(self: Box<Self>) -> Option<Box<dyn Template + 'a>> {
897 (*self).try_into_template()
898 }
899
900 fn try_join(self: Box<Self>, _: Box<dyn Template + 'a>) -> Option<Box<dyn Template + 'a>> {
901 None
902 }
903}
904
905pub struct BuildContext<'i, P> {
907 local_variables: HashMap<&'i str, &'i dyn Fn() -> P>,
909 self_variable: &'i dyn Fn() -> P,
914}
915
916fn build_keyword<'a, L: TemplateLanguage<'a> + ?Sized>(
917 language: &L,
918 diagnostics: &mut TemplateDiagnostics,
919 build_ctx: &BuildContext<L::Property>,
920 name: &str,
921 name_span: pest::Span<'_>,
922) -> TemplateParseResult<L::Property> {
923 let self_property = (build_ctx.self_variable)();
925 let function = FunctionCallNode {
926 name,
927 name_span,
928 args: vec![],
929 keyword_args: vec![],
930 args_span: name_span.end_pos().span(&name_span.end_pos()),
931 };
932 language
933 .build_method(diagnostics, build_ctx, self_property, &function)
934 .map_err(|err| match err.kind() {
935 TemplateParseErrorKind::NoSuchMethod { candidates, .. } => {
936 let kind = TemplateParseErrorKind::NoSuchKeyword {
937 name: name.to_owned(),
938 candidates: candidates.clone(),
940 };
941 TemplateParseError::with_span(kind, name_span)
942 }
943 TemplateParseErrorKind::InvalidArguments { .. } => {
946 let kind = TemplateParseErrorKind::NoSuchKeyword {
947 name: name.to_owned(),
948 candidates: vec![format!("self.{name}(..)")],
950 };
951 TemplateParseError::with_span(kind, name_span)
952 }
953 _ => err,
955 })
956}
957
958fn build_unary_operation<'a, L: TemplateLanguage<'a> + ?Sized>(
959 language: &L,
960 diagnostics: &mut TemplateDiagnostics,
961 build_ctx: &BuildContext<L::Property>,
962 op: UnaryOp,
963 arg_node: &ExpressionNode,
964) -> TemplateParseResult<L::Property> {
965 match op {
966 UnaryOp::LogicalNot => {
967 let arg = expect_boolean_expression(language, diagnostics, build_ctx, arg_node)?;
968 Ok(arg.map(|v| !v).into_dyn_wrapped())
969 }
970 UnaryOp::Negate => {
971 let arg = expect_integer_expression(language, diagnostics, build_ctx, arg_node)?;
972 let out = arg.and_then(|v| {
973 v.checked_neg()
974 .ok_or_else(|| TemplatePropertyError("Attempt to negate with overflow".into()))
975 });
976 Ok(out.into_dyn_wrapped())
977 }
978 }
979}
980
981fn build_binary_operation<'a, L: TemplateLanguage<'a> + ?Sized>(
982 language: &L,
983 diagnostics: &mut TemplateDiagnostics,
984 build_ctx: &BuildContext<L::Property>,
985 op: BinaryOp,
986 lhs_node: &ExpressionNode,
987 rhs_node: &ExpressionNode,
988 span: pest::Span<'_>,
989) -> TemplateParseResult<L::Property> {
990 match op {
991 BinaryOp::LogicalOr => {
992 let lhs = expect_boolean_expression(language, diagnostics, build_ctx, lhs_node)?;
993 let rhs = expect_boolean_expression(language, diagnostics, build_ctx, rhs_node)?;
994 let out = lhs.and_then(move |l| Ok(l || rhs.extract()?));
995 Ok(out.into_dyn_wrapped())
996 }
997 BinaryOp::LogicalAnd => {
998 let lhs = expect_boolean_expression(language, diagnostics, build_ctx, lhs_node)?;
999 let rhs = expect_boolean_expression(language, diagnostics, build_ctx, rhs_node)?;
1000 let out = lhs.and_then(move |l| Ok(l && rhs.extract()?));
1001 Ok(out.into_dyn_wrapped())
1002 }
1003 BinaryOp::Eq | BinaryOp::Ne => {
1004 let lhs = build_expression(language, diagnostics, build_ctx, lhs_node)?;
1005 let rhs = build_expression(language, diagnostics, build_ctx, rhs_node)?;
1006 let lty = lhs.type_name();
1007 let rty = rhs.type_name();
1008 let eq = lhs.try_into_eq(rhs).ok_or_else(|| {
1009 let message = format!("Cannot compare expressions of type `{lty}` and `{rty}`");
1010 TemplateParseError::expression(message, span)
1011 })?;
1012 let out = match op {
1013 BinaryOp::Eq => eq.into_dyn(),
1014 BinaryOp::Ne => eq.map(|eq| !eq).into_dyn(),
1015 _ => unreachable!(),
1016 };
1017 Ok(L::Property::wrap_property(out))
1018 }
1019 BinaryOp::Ge | BinaryOp::Gt | BinaryOp::Le | BinaryOp::Lt => {
1020 let lhs = build_expression(language, diagnostics, build_ctx, lhs_node)?;
1021 let rhs = build_expression(language, diagnostics, build_ctx, rhs_node)?;
1022 let lty = lhs.type_name();
1023 let rty = rhs.type_name();
1024 let cmp = lhs.try_into_cmp(rhs).ok_or_else(|| {
1025 let message = format!("Cannot compare expressions of type `{lty}` and `{rty}`");
1026 TemplateParseError::expression(message, span)
1027 })?;
1028 let out = match op {
1029 BinaryOp::Ge => cmp.map(|ordering| ordering.is_ge()).into_dyn(),
1030 BinaryOp::Gt => cmp.map(|ordering| ordering.is_gt()).into_dyn(),
1031 BinaryOp::Le => cmp.map(|ordering| ordering.is_le()).into_dyn(),
1032 BinaryOp::Lt => cmp.map(|ordering| ordering.is_lt()).into_dyn(),
1033 _ => unreachable!(),
1034 };
1035 Ok(L::Property::wrap_property(out))
1036 }
1037 BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div | BinaryOp::Rem => {
1038 let lhs = expect_integer_expression(language, diagnostics, build_ctx, lhs_node)?;
1039 let rhs = expect_integer_expression(language, diagnostics, build_ctx, rhs_node)?;
1040 let build = |op: fn(i64, i64) -> Option<i64>, msg: fn(i64) -> &'static str| {
1041 (lhs, rhs).and_then(move |(l, r)| {
1042 op(l, r).ok_or_else(|| TemplatePropertyError(msg(r).into()))
1043 })
1044 };
1045 let out = match op {
1046 BinaryOp::Add => build(i64::checked_add, |_| "Attempt to add with overflow"),
1047 BinaryOp::Sub => build(i64::checked_sub, |_| "Attempt to subtract with overflow"),
1048 BinaryOp::Mul => build(i64::checked_mul, |_| "Attempt to multiply with overflow"),
1049 BinaryOp::Div => build(i64::checked_div, |r| {
1050 if r == 0 {
1051 "Attempt to divide by zero"
1052 } else {
1053 "Attempt to divide with overflow"
1054 }
1055 }),
1056 BinaryOp::Rem => build(i64::checked_rem, |r| {
1057 if r == 0 {
1058 "Attempt to divide by zero"
1059 } else {
1060 "Attempt to divide with overflow"
1061 }
1062 }),
1063 _ => unreachable!(),
1064 };
1065 Ok(out.into_dyn_wrapped())
1066 }
1067 }
1068}
1069
1070fn builtin_byte_string_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1071-> TemplateBuildMethodFnMap<'a, L, BString> {
1072 let mut map = TemplateBuildMethodFnMap::<L, BString>::new();
1075 map.insert(
1076 "len",
1077 |_language, _diagnostics, _build_ctx, self_property, function| {
1078 function.expect_no_arguments()?;
1079 let out_property = self_property.and_then(|s| Ok(i64::try_from(s.len())?));
1080 Ok(out_property.into_dyn_wrapped())
1081 },
1082 );
1083 map.insert(
1084 "contains",
1085 |language, diagnostics, build_ctx, self_property, function| {
1086 let [needle_node] = function.expect_exact_arguments()?;
1087 let needle_property =
1089 expect_byte_stringify_expression(language, diagnostics, build_ctx, needle_node)?;
1090 let out_property = (self_property, needle_property)
1091 .map(|(haystack, needle)| haystack.contains_str(&needle));
1092 Ok(out_property.into_dyn_wrapped())
1093 },
1094 );
1095 map.insert(
1096 "match",
1097 |_language, _diagnostics, _build_ctx, self_property, function| {
1098 let [needle_node] = function.expect_exact_arguments()?;
1099 let needle = template_parser::expect_string_pattern(needle_node)?;
1100 let out_property = match_string_like(self_property, needle);
1101 Ok(out_property.into_dyn_wrapped())
1102 },
1103 );
1104 map.insert(
1105 "starts_with",
1106 |language, diagnostics, build_ctx, self_property, function| {
1107 let [needle_node] = function.expect_exact_arguments()?;
1108 let needle_property =
1109 expect_byte_stringify_expression(language, diagnostics, build_ctx, needle_node)?;
1110 let out_property = (self_property, needle_property)
1111 .map(|(haystack, needle)| haystack.starts_with(&needle));
1112 Ok(out_property.into_dyn_wrapped())
1113 },
1114 );
1115 map.insert(
1116 "ends_with",
1117 |language, diagnostics, build_ctx, self_property, function| {
1118 let [needle_node] = function.expect_exact_arguments()?;
1119 let needle_property =
1120 expect_byte_stringify_expression(language, diagnostics, build_ctx, needle_node)?;
1121 let out_property = (self_property, needle_property)
1122 .map(|(haystack, needle)| haystack.ends_with(&needle));
1123 Ok(out_property.into_dyn_wrapped())
1124 },
1125 );
1126 map.insert(
1127 "remove_prefix",
1128 |language, diagnostics, build_ctx, self_property, function| {
1129 let [needle_node] = function.expect_exact_arguments()?;
1130 let needle_property =
1131 expect_byte_stringify_expression(language, diagnostics, build_ctx, needle_node)?;
1132 let out_property = (self_property, needle_property).map(|(haystack, needle)| {
1133 haystack
1134 .strip_prefix(&**needle)
1135 .map(BString::from)
1136 .unwrap_or(haystack)
1137 });
1138 Ok(out_property.into_dyn_wrapped())
1139 },
1140 );
1141 map.insert(
1142 "remove_suffix",
1143 |language, diagnostics, build_ctx, self_property, function| {
1144 let [needle_node] = function.expect_exact_arguments()?;
1145 let needle_property =
1146 expect_byte_stringify_expression(language, diagnostics, build_ctx, needle_node)?;
1147 let out_property = (self_property, needle_property).map(|(haystack, needle)| {
1148 haystack
1149 .strip_suffix(&**needle)
1150 .map(BString::from)
1151 .unwrap_or(haystack)
1152 });
1153 Ok(out_property.into_dyn_wrapped())
1154 },
1155 );
1156 map.insert(
1157 "trim",
1158 |_language, _diagnostics, _build_ctx, self_property, function| {
1159 function.expect_no_arguments()?;
1160 let out_property = self_property.map(|s| BString::from(s.trim_ascii()));
1161 Ok(out_property.into_dyn_wrapped())
1162 },
1163 );
1164 map.insert(
1165 "trim_start",
1166 |_language, _diagnostics, _build_ctx, self_property, function| {
1167 function.expect_no_arguments()?;
1168 let out_property = self_property.map(|s| BString::from(s.trim_ascii_start()));
1169 Ok(out_property.into_dyn_wrapped())
1170 },
1171 );
1172 map.insert(
1173 "trim_end",
1174 |_language, _diagnostics, _build_ctx, self_property, function| {
1175 function.expect_no_arguments()?;
1176 let out_property = self_property.map(|s| BString::from(s.trim_ascii_end()));
1177 Ok(out_property.into_dyn_wrapped())
1178 },
1179 );
1180 map.insert(
1181 "substr",
1182 |language, diagnostics, build_ctx, self_property, function| {
1183 let ([start_idx_node], [end_idx_node]) = function.expect_arguments()?;
1184 let start_idx_property =
1185 expect_isize_expression(language, diagnostics, build_ctx, start_idx_node)?;
1186 let end_idx_property = end_idx_node
1187 .map(|node| expect_isize_expression(language, diagnostics, build_ctx, node))
1188 .transpose()?;
1189 let out_property = (self_property, start_idx_property, end_idx_property).map(
1190 |(s, start_idx, end_idx)| {
1191 let start_idx = clamp_signed_bytes_index(&s, start_idx);
1192 let end_idx = end_idx.map_or(s.len(), |idx| clamp_signed_bytes_index(&s, idx));
1193 BString::from(s.get(start_idx..end_idx).unwrap_or_default())
1194 },
1195 );
1196 Ok(out_property.into_dyn_wrapped())
1197 },
1198 );
1199 map.insert(
1200 "first_line",
1201 |_language, _diagnostics, _build_ctx, self_property, function| {
1202 function.expect_no_arguments()?;
1203 let out_property =
1204 self_property.map(|s| s.lines().next().map(BString::from).unwrap_or_default());
1205 Ok(out_property.into_dyn_wrapped())
1206 },
1207 );
1208 map.insert(
1209 "lines",
1210 |_language, _diagnostics, _build_ctx, self_property, function| {
1211 function.expect_no_arguments()?;
1212 let out_property = self_property.map(|s| s.lines().map(BString::from).collect_vec());
1213 Ok(out_property.into_dyn_wrapped())
1214 },
1215 );
1216 map.insert(
1217 "split",
1218 |language, diagnostics, build_ctx, self_property, function| {
1219 let ([separator_node], [limit_node]) = function.expect_arguments()?;
1220 let pattern = template_parser::expect_string_pattern(separator_node)?;
1221 if let Some(limit_node) = limit_node {
1222 let limit_property =
1223 expect_usize_expression(language, diagnostics, build_ctx, limit_node)?;
1224 let out_property = splitn_string_like(self_property, pattern, limit_property);
1225 Ok(out_property.into_dyn_wrapped())
1226 } else {
1227 let out_property = split_string_like(self_property, pattern);
1228 Ok(out_property.into_dyn_wrapped())
1229 }
1230 },
1231 );
1232 map.insert(
1233 "replace",
1234 |language, diagnostics, build_ctx, self_property, function| {
1235 let ([pattern_node, replacement_node], [limit_node]) = function.expect_arguments()?;
1236 let pattern = template_parser::expect_string_pattern(pattern_node)?;
1237 let replacement_property = expect_byte_stringify_expression(
1238 language,
1239 diagnostics,
1240 build_ctx,
1241 replacement_node,
1242 )?;
1243 if let Some(limit_node) = limit_node {
1244 let limit_property =
1245 expect_usize_expression(language, diagnostics, build_ctx, limit_node)?;
1246 let out_property = replacen_string_like(
1247 self_property,
1248 pattern,
1249 replacement_property,
1250 limit_property,
1251 );
1252 Ok(out_property.into_dyn_wrapped())
1253 } else {
1254 let out_property =
1255 replace_all_string_like(self_property, pattern, replacement_property);
1256 Ok(out_property.into_dyn_wrapped())
1257 }
1258 },
1259 );
1260 map.insert(
1261 "upper",
1262 |_language, _diagnostics, _build_ctx, self_property, function| {
1263 function.expect_no_arguments()?;
1264 let out_property = self_property.map(|s| BString::from(s.to_ascii_uppercase()));
1265 Ok(out_property.into_dyn_wrapped())
1266 },
1267 );
1268 map.insert(
1269 "lower",
1270 |_language, _diagnostics, _build_ctx, self_property, function| {
1271 function.expect_no_arguments()?;
1272 let out_property = self_property.map(|s| BString::from(s.to_ascii_lowercase()));
1273 Ok(out_property.into_dyn_wrapped())
1274 },
1275 );
1276 map
1277}
1278
1279fn builtin_string_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1280-> TemplateBuildMethodFnMap<'a, L, String> {
1281 let mut map = TemplateBuildMethodFnMap::<L, String>::new();
1284 map.insert(
1285 "len",
1286 |_language, _diagnostics, _build_ctx, self_property, function| {
1287 function.expect_no_arguments()?;
1288 let out_property = self_property.and_then(|s| Ok(i64::try_from(s.len())?));
1289 Ok(out_property.into_dyn_wrapped())
1290 },
1291 );
1292 map.insert(
1293 "contains",
1294 |language, diagnostics, build_ctx, self_property, function| {
1295 let [needle_node] = function.expect_exact_arguments()?;
1296 let needle_property =
1298 expect_stringify_expression(language, diagnostics, build_ctx, needle_node)?;
1299 let out_property = (self_property, needle_property)
1300 .map(|(haystack, needle)| haystack.contains(&needle));
1301 Ok(out_property.into_dyn_wrapped())
1302 },
1303 );
1304 map.insert(
1305 "match",
1306 |_language, _diagnostics, _build_ctx, self_property, function| {
1307 let [needle_node] = function.expect_exact_arguments()?;
1308 let needle = template_parser::expect_string_pattern(needle_node)?;
1309 let out_property = match_string_like(self_property, needle);
1310 Ok(out_property.into_dyn_wrapped())
1311 },
1312 );
1313 map.insert(
1314 "starts_with",
1315 |language, diagnostics, build_ctx, self_property, function| {
1316 let [needle_node] = function.expect_exact_arguments()?;
1317 let needle_property =
1318 expect_stringify_expression(language, diagnostics, build_ctx, needle_node)?;
1319 let out_property = (self_property, needle_property)
1320 .map(|(haystack, needle)| haystack.starts_with(&needle));
1321 Ok(out_property.into_dyn_wrapped())
1322 },
1323 );
1324 map.insert(
1325 "ends_with",
1326 |language, diagnostics, build_ctx, self_property, function| {
1327 let [needle_node] = function.expect_exact_arguments()?;
1328 let needle_property =
1329 expect_stringify_expression(language, diagnostics, build_ctx, needle_node)?;
1330 let out_property = (self_property, needle_property)
1331 .map(|(haystack, needle)| haystack.ends_with(&needle));
1332 Ok(out_property.into_dyn_wrapped())
1333 },
1334 );
1335 map.insert(
1336 "remove_prefix",
1337 |language, diagnostics, build_ctx, self_property, function| {
1338 let [needle_node] = function.expect_exact_arguments()?;
1339 let needle_property =
1340 expect_stringify_expression(language, diagnostics, build_ctx, needle_node)?;
1341 let out_property = (self_property, needle_property).map(|(haystack, needle)| {
1342 haystack
1343 .strip_prefix(&needle)
1344 .map(ToOwned::to_owned)
1345 .unwrap_or(haystack)
1346 });
1347 Ok(out_property.into_dyn_wrapped())
1348 },
1349 );
1350 map.insert(
1351 "remove_suffix",
1352 |language, diagnostics, build_ctx, self_property, function| {
1353 let [needle_node] = function.expect_exact_arguments()?;
1354 let needle_property =
1355 expect_stringify_expression(language, diagnostics, build_ctx, needle_node)?;
1356 let out_property = (self_property, needle_property).map(|(haystack, needle)| {
1357 haystack
1358 .strip_suffix(&needle)
1359 .map(ToOwned::to_owned)
1360 .unwrap_or(haystack)
1361 });
1362 Ok(out_property.into_dyn_wrapped())
1363 },
1364 );
1365 map.insert(
1366 "trim",
1367 |_language, _diagnostics, _build_ctx, self_property, function| {
1368 function.expect_no_arguments()?;
1369 let out_property = self_property.map(|s| s.trim().to_owned());
1370 Ok(out_property.into_dyn_wrapped())
1371 },
1372 );
1373 map.insert(
1374 "trim_start",
1375 |_language, _diagnostics, _build_ctx, self_property, function| {
1376 function.expect_no_arguments()?;
1377 let out_property = self_property.map(|s| s.trim_start().to_owned());
1378 Ok(out_property.into_dyn_wrapped())
1379 },
1380 );
1381 map.insert(
1382 "trim_end",
1383 |_language, _diagnostics, _build_ctx, self_property, function| {
1384 function.expect_no_arguments()?;
1385 let out_property = self_property.map(|s| s.trim_end().to_owned());
1386 Ok(out_property.into_dyn_wrapped())
1387 },
1388 );
1389 map.insert(
1390 "substr",
1391 |language, diagnostics, build_ctx, self_property, function| {
1392 let ([start_idx_node], [end_idx_node]) = function.expect_arguments()?;
1393 let start_idx_property =
1394 expect_isize_expression(language, diagnostics, build_ctx, start_idx_node)?;
1395 let end_idx_property = end_idx_node
1396 .map(|node| expect_isize_expression(language, diagnostics, build_ctx, node))
1397 .transpose()?;
1398 let out_property = (self_property, start_idx_property, end_idx_property).map(
1399 |(s, start_idx, end_idx)| {
1400 let start_idx = string_index_to_char_boundary(&s, start_idx);
1401 let end_idx =
1402 end_idx.map_or(s.len(), |idx| string_index_to_char_boundary(&s, idx));
1403 s.get(start_idx..end_idx).unwrap_or_default().to_owned()
1404 },
1405 );
1406 Ok(out_property.into_dyn_wrapped())
1407 },
1408 );
1409 map.insert(
1410 "first_line",
1411 |_language, _diagnostics, _build_ctx, self_property, function| {
1412 function.expect_no_arguments()?;
1413 let out_property =
1414 self_property.map(|s| s.lines().next().unwrap_or_default().to_string());
1415 Ok(out_property.into_dyn_wrapped())
1416 },
1417 );
1418 map.insert(
1419 "lines",
1420 |_language, _diagnostics, _build_ctx, self_property, function| {
1421 function.expect_no_arguments()?;
1422 let out_property = self_property.map(|s| s.lines().map(|l| l.to_owned()).collect_vec());
1423 Ok(out_property.into_dyn_wrapped())
1424 },
1425 );
1426 map.insert(
1427 "split",
1428 |language, diagnostics, build_ctx, self_property, function| {
1429 let ([separator_node], [limit_node]) = function.expect_arguments()?;
1430 let pattern = template_parser::expect_string_pattern(separator_node)?;
1431 if let Some(limit_node) = limit_node {
1432 let limit_property =
1433 expect_usize_expression(language, diagnostics, build_ctx, limit_node)?;
1434 let out_property = splitn_string_like(self_property, pattern, limit_property);
1435 Ok(out_property.into_dyn_wrapped())
1436 } else {
1437 let out_property = split_string_like(self_property, pattern);
1438 Ok(out_property.into_dyn_wrapped())
1439 }
1440 },
1441 );
1442 map.insert(
1443 "replace",
1444 |language, diagnostics, build_ctx, self_property, function| {
1445 let ([pattern_node, replacement_node], [limit_node]) = function.expect_arguments()?;
1446 let pattern = template_parser::expect_string_pattern(pattern_node)?;
1447 let replacement_property =
1448 expect_stringify_expression(language, diagnostics, build_ctx, replacement_node)?;
1449 if let Some(limit_node) = limit_node {
1450 let limit_property =
1451 expect_usize_expression(language, diagnostics, build_ctx, limit_node)?;
1452 let out_property = replacen_string_like(
1453 self_property,
1454 pattern,
1455 replacement_property,
1456 limit_property,
1457 );
1458 Ok(out_property.into_dyn_wrapped())
1459 } else {
1460 let out_property =
1461 replace_all_string_like(self_property, pattern, replacement_property);
1462 Ok(out_property.into_dyn_wrapped())
1463 }
1464 },
1465 );
1466 map.insert(
1467 "upper",
1468 |_language, _diagnostics, _build_ctx, self_property, function| {
1469 function.expect_no_arguments()?;
1470 let out_property = self_property.map(|s| s.to_uppercase());
1471 Ok(out_property.into_dyn_wrapped())
1472 },
1473 );
1474 map.insert(
1475 "lower",
1476 |_language, _diagnostics, _build_ctx, self_property, function| {
1477 function.expect_no_arguments()?;
1478 let out_property = self_property.map(|s| s.to_lowercase());
1479 Ok(out_property.into_dyn_wrapped())
1480 },
1481 );
1482 map.insert(
1483 "escape_json",
1484 |_language, _diagnostics, _build_ctx, self_property, function| {
1485 function.expect_no_arguments()?;
1486 let out_property = self_property.map(|s| serde_json::to_string(&s).unwrap());
1487 Ok(out_property.into_dyn_wrapped())
1488 },
1489 );
1490 map
1491}
1492
1493trait StringLike: AsRef<[u8]> + Sized {
1494 fn from_bytes(bytes: &[u8]) -> Result<Self, TemplatePropertyError>;
1495}
1496
1497impl StringLike for BString {
1498 fn from_bytes(bytes: &[u8]) -> Result<Self, TemplatePropertyError> {
1499 Ok(bytes.into())
1500 }
1501}
1502
1503impl StringLike for String {
1504 fn from_bytes(bytes: &[u8]) -> Result<Self, TemplatePropertyError> {
1505 Ok(str::from_utf8(bytes)?.to_owned())
1506 }
1507}
1508
1509fn match_string_like<S: StringLike>(
1510 self_property: impl TemplateProperty<Output = S>,
1511 needle: StringPattern,
1512) -> impl TemplateProperty<Output = S> {
1513 let regex = needle.to_regex();
1514 self_property.and_then(move |haystack| {
1515 S::from_bytes(regex.find(haystack.as_ref()).map_or(b"", |m| m.as_bytes()))
1517 })
1518}
1519
1520fn split_string_like<S: StringLike>(
1521 self_property: impl TemplateProperty<Output = S>,
1522 pattern: StringPattern,
1523) -> impl TemplateProperty<Output = Vec<S>> {
1524 let regex = pattern.to_regex();
1525 self_property
1526 .and_then(move |haystack| regex.split(haystack.as_ref()).map(S::from_bytes).collect())
1527}
1528
1529fn splitn_string_like<S: StringLike>(
1530 self_property: impl TemplateProperty<Output = S>,
1531 pattern: StringPattern,
1532 limit_property: impl TemplateProperty<Output = usize>,
1533) -> impl TemplateProperty<Output = Vec<S>> {
1534 let regex = pattern.to_regex();
1535 (self_property, limit_property).and_then(move |(haystack, limit)| {
1536 regex
1537 .splitn(haystack.as_ref(), limit)
1538 .map(S::from_bytes)
1539 .collect()
1540 })
1541}
1542
1543fn replace_all_string_like<S: StringLike>(
1544 self_property: impl TemplateProperty<Output = S>,
1545 pattern: StringPattern,
1546 replacement_property: impl TemplateProperty<Output = S>,
1547) -> impl TemplateProperty<Output = S> {
1548 let regex = pattern.to_regex();
1549 (self_property, replacement_property).and_then(move |(haystack, replacement)| {
1550 S::from_bytes(®ex.replace_all(haystack.as_ref(), replacement.as_ref()))
1551 })
1552}
1553
1554fn replacen_string_like<S: StringLike>(
1555 self_property: impl TemplateProperty<Output = S>,
1556 pattern: StringPattern,
1557 replacement_property: impl TemplateProperty<Output = S>,
1558 limit_property: impl TemplateProperty<Output = usize>,
1559) -> impl TemplateProperty<Output = S> {
1560 let regex = pattern.to_regex();
1561 (self_property, replacement_property, limit_property).and_then(
1562 move |(haystack, replacement, limit)| {
1563 if limit == 0 {
1564 Ok(haystack)
1568 } else {
1569 S::from_bytes(®ex.replacen(haystack.as_ref(), limit, replacement.as_ref()))
1570 }
1571 },
1572 )
1573}
1574
1575fn clamp_signed_bytes_index(s: &[u8], i: isize) -> usize {
1577 let magnitude = i.unsigned_abs();
1578 if i < 0 {
1579 s.len().saturating_sub(magnitude)
1580 } else {
1581 magnitude.min(s.len())
1582 }
1583}
1584
1585fn string_index_to_char_boundary(s: &str, i: isize) -> usize {
1590 let p = clamp_signed_bytes_index(s.as_bytes(), i);
1592 if i < 0 {
1593 (p..=s.len()).find(|&p| s.is_char_boundary(p)).unwrap()
1594 } else {
1595 (0..=p).rev().find(|&p| s.is_char_boundary(p)).unwrap()
1596 }
1597}
1598
1599fn builtin_config_value_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1600-> TemplateBuildMethodFnMap<'a, L, ConfigValue> {
1601 fn extract<'de, T: Deserialize<'de>>(value: ConfigValue) -> Result<T, TemplatePropertyError> {
1602 T::deserialize(value.into_deserializer())
1603 .map_err(|err| TemplatePropertyError(err.message().into()))
1605 }
1606
1607 let mut map = TemplateBuildMethodFnMap::<L, ConfigValue>::new();
1610 map.insert(
1615 "as_boolean",
1616 |_language, _diagnostics, _build_ctx, self_property, function| {
1617 function.expect_no_arguments()?;
1618 let out_property = self_property.and_then(extract::<bool>);
1619 Ok(out_property.into_dyn_wrapped())
1620 },
1621 );
1622 map.insert(
1623 "as_integer",
1624 |_language, _diagnostics, _build_ctx, self_property, function| {
1625 function.expect_no_arguments()?;
1626 let out_property = self_property.and_then(extract::<i64>);
1627 Ok(out_property.into_dyn_wrapped())
1628 },
1629 );
1630 map.insert(
1631 "as_string",
1632 |_language, _diagnostics, _build_ctx, self_property, function| {
1633 function.expect_no_arguments()?;
1634 let out_property = self_property.and_then(extract::<String>);
1635 Ok(out_property.into_dyn_wrapped())
1636 },
1637 );
1638 map.insert(
1639 "as_string_list",
1640 |_language, _diagnostics, _build_ctx, self_property, function| {
1641 function.expect_no_arguments()?;
1642 let out_property = self_property.and_then(extract::<Vec<String>>);
1643 Ok(out_property.into_dyn_wrapped())
1644 },
1645 );
1646 map
1649}
1650
1651fn builtin_signature_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1652-> TemplateBuildMethodFnMap<'a, L, Signature> {
1653 let mut map = TemplateBuildMethodFnMap::<L, Signature>::new();
1656 map.insert(
1657 "name",
1658 |_language, _diagnostics, _build_ctx, self_property, function| {
1659 function.expect_no_arguments()?;
1660 let out_property = self_property.map(|signature| signature.name);
1661 Ok(out_property.into_dyn_wrapped())
1662 },
1663 );
1664 map.insert(
1665 "email",
1666 |_language, _diagnostics, _build_ctx, self_property, function| {
1667 function.expect_no_arguments()?;
1668 let out_property = self_property.map(|signature| Email(signature.email));
1669 Ok(out_property.into_dyn_wrapped())
1670 },
1671 );
1672 map.insert(
1673 "timestamp",
1674 |_language, _diagnostics, _build_ctx, self_property, function| {
1675 function.expect_no_arguments()?;
1676 let out_property = self_property.map(|signature| signature.timestamp);
1677 Ok(out_property.into_dyn_wrapped())
1678 },
1679 );
1680 map
1681}
1682
1683fn builtin_email_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1684-> TemplateBuildMethodFnMap<'a, L, Email> {
1685 let mut map = TemplateBuildMethodFnMap::<L, Email>::new();
1688 map.insert(
1689 "local",
1690 |_language, _diagnostics, _build_ctx, self_property, function| {
1691 function.expect_no_arguments()?;
1692 let out_property = self_property.map(|email| {
1693 let (local, _) = text_util::split_email(&email.0);
1694 local.to_owned()
1695 });
1696 Ok(out_property.into_dyn_wrapped())
1697 },
1698 );
1699 map.insert(
1700 "domain",
1701 |_language, _diagnostics, _build_ctx, self_property, function| {
1702 function.expect_no_arguments()?;
1703 let out_property = self_property.map(|email| {
1704 let (_, domain) = text_util::split_email(&email.0);
1705 domain.unwrap_or_default().to_owned()
1706 });
1707 Ok(out_property.into_dyn_wrapped())
1708 },
1709 );
1710 map
1711}
1712
1713fn builtin_size_hint_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1714-> TemplateBuildMethodFnMap<'a, L, SizeHint> {
1715 let mut map = TemplateBuildMethodFnMap::<L, SizeHint>::new();
1718 map.insert(
1719 "lower",
1720 |_language, _diagnostics, _build_ctx, self_property, function| {
1721 function.expect_no_arguments()?;
1722 let out_property = self_property.and_then(|(lower, _)| Ok(i64::try_from(lower)?));
1723 Ok(out_property.into_dyn_wrapped())
1724 },
1725 );
1726 map.insert(
1727 "upper",
1728 |_language, _diagnostics, _build_ctx, self_property, function| {
1729 function.expect_no_arguments()?;
1730 let out_property =
1731 self_property.and_then(|(_, upper)| Ok(upper.map(i64::try_from).transpose()?));
1732 Ok(out_property.into_dyn_wrapped())
1733 },
1734 );
1735 map.insert(
1736 "exact",
1737 |_language, _diagnostics, _build_ctx, self_property, function| {
1738 function.expect_no_arguments()?;
1739 let out_property = self_property.and_then(|(lower, upper)| {
1740 let exact = (Some(lower) == upper).then_some(lower);
1741 Ok(exact.map(i64::try_from).transpose()?)
1742 });
1743 Ok(out_property.into_dyn_wrapped())
1744 },
1745 );
1746 map.insert(
1747 "zero",
1748 |_language, _diagnostics, _build_ctx, self_property, function| {
1749 function.expect_no_arguments()?;
1750 let out_property = self_property.map(|(_, upper)| upper == Some(0));
1751 Ok(out_property.into_dyn_wrapped())
1752 },
1753 );
1754 map
1755}
1756
1757fn builtin_regex_captures_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1758-> TemplateBuildMethodFnMap<'a, L, RegexCaptures> {
1759 let mut map = TemplateBuildMethodFnMap::<L, RegexCaptures>::new();
1762 map.insert(
1763 "len",
1764 |_language, _diagnostics, _build_ctx, self_property, function| {
1765 function.expect_no_arguments()?;
1766 let out_property =
1767 self_property.and_then(|captures| Ok(i64::try_from(captures.len())?));
1768 Ok(out_property.into_dyn_wrapped())
1769 },
1770 );
1771 map.insert(
1772 "get",
1773 |language, diagnostics, build_ctx, self_property, function| {
1774 let [index_node] = function.expect_exact_arguments()?;
1775 let index = expect_usize_expression(language, diagnostics, build_ctx, index_node)?;
1776 let out_property = (self_property, index).and_then(|(captures, index)| {
1777 captures.get(index).ok_or_else(|| {
1778 TemplatePropertyError(
1779 format!("Could not get capture group with index {index}").into(),
1780 )
1781 })
1782 });
1783 Ok(out_property.into_dyn_wrapped())
1784 },
1785 );
1786 map.insert(
1787 "name",
1788 |language, diagnostics, build_ctx, self_property, function| {
1789 let [name_node] = function.expect_exact_arguments()?;
1790 let name = expect_stringify_expression(language, diagnostics, build_ctx, name_node)?;
1791 let out_property = (self_property, name).and_then(move |(c, name)| {
1792 c.name(&name).ok_or_else(|| {
1793 TemplatePropertyError(
1794 format!("Could not get capture group with name {name}").into(),
1795 )
1796 })
1797 });
1798 Ok(out_property.into_dyn_wrapped())
1799 },
1800 );
1801 map
1802}
1803
1804fn builtin_timestamp_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1805-> TemplateBuildMethodFnMap<'a, L, Timestamp> {
1806 let mut map = TemplateBuildMethodFnMap::<L, Timestamp>::new();
1809 map.insert(
1810 "ago",
1811 |_language, _diagnostics, _build_ctx, self_property, function| {
1812 function.expect_no_arguments()?;
1813 let now = Timestamp::now();
1814 let format = timeago::Formatter::new();
1815 let out_property = self_property.and_then(move |timestamp| {
1816 Ok(time_util::format_duration(×tamp, &now, &format)?)
1817 });
1818 Ok(out_property.into_dyn_wrapped())
1819 },
1820 );
1821 map.insert(
1822 "format",
1823 |language, diagnostics, build_ctx, self_property, function| {
1824 let [format_node] = function.expect_exact_arguments()?;
1825 let format_property =
1826 expect_stringify_expression(language, diagnostics, build_ctx, format_node)?;
1827 if let Ok(format) = format_property.extract() {
1828 let format = template_parser::catch_aliases(
1829 diagnostics,
1830 format_node,
1831 |_diagnostics, node| {
1832 time_util::FormattingItems::parse(&format).ok_or_else(|| {
1833 TemplateParseError::expression("Invalid time format", node.span)
1834 })
1835 },
1836 )?
1837 .into_owned();
1838 let out_property = self_property.and_then(move |timestamp| {
1839 Ok(time_util::format_absolute_timestamp_with(
1840 ×tamp, &format,
1841 )?)
1842 });
1843 Ok(out_property.into_dyn_wrapped())
1844 } else {
1845 let out_property =
1846 (self_property, format_property).and_then(move |(timestamp, format)| {
1847 let format =
1848 time_util::FormattingItems::parse(&format).ok_or_else(|| {
1849 let message = format!("Invalid time format: {format}");
1850 TemplatePropertyError(message.into())
1851 })?;
1852 Ok(time_util::format_absolute_timestamp_with(
1853 ×tamp, &format,
1854 )?)
1855 });
1856 Ok(out_property.into_dyn_wrapped())
1857 }
1858 },
1859 );
1860 map.insert(
1861 "utc",
1862 |_language, _diagnostics, _build_ctx, self_property, function| {
1863 function.expect_no_arguments()?;
1864 let out_property = self_property.map(|mut timestamp| {
1865 timestamp.tz_offset = 0;
1866 timestamp
1867 });
1868 Ok(out_property.into_dyn_wrapped())
1869 },
1870 );
1871 map.insert(
1872 "local",
1873 |_language, _diagnostics, _build_ctx, self_property, function| {
1874 function.expect_no_arguments()?;
1875 let tz_offset = std::env::var("JJ_TZ_OFFSET_MINS")
1876 .ok()
1877 .and_then(|tz_string| tz_string.parse::<i32>().ok())
1878 .unwrap_or_else(|| chrono::Local::now().offset().local_minus_utc() / 60);
1879 let out_property = self_property.map(move |mut timestamp| {
1880 timestamp.tz_offset = tz_offset;
1881 timestamp
1882 });
1883 Ok(out_property.into_dyn_wrapped())
1884 },
1885 );
1886 map.insert(
1887 "after",
1888 |_language, diagnostics, _build_ctx, self_property, function| {
1889 let [date_pattern_node] = function.expect_exact_arguments()?;
1890 let now = chrono::Local::now();
1891 let date_pattern = template_parser::catch_aliases(
1892 diagnostics,
1893 date_pattern_node,
1894 |_diagnostics, node| {
1895 let date_pattern = template_parser::expect_string_literal(node)?;
1896 DatePattern::from_str_kind(date_pattern, function.name, now).map_err(|err| {
1897 TemplateParseError::expression("Invalid date pattern", node.span)
1898 .with_source(err)
1899 })
1900 },
1901 )?;
1902 let out_property = self_property.map(move |timestamp| date_pattern.matches(×tamp));
1903 Ok(out_property.into_dyn_wrapped())
1904 },
1905 );
1906 map.insert("before", map["after"]);
1907 map.insert(
1908 "since",
1909 |language, diagnostics, build_ctx, self_property, function| {
1910 let [date_node] = function.expect_exact_arguments()?;
1911 let date_property =
1912 expect_timestamp_expression(language, diagnostics, build_ctx, date_node)?;
1913 let out_property =
1914 (self_property, date_property).and_then(move |(self_timestamp, arg_timestamp)| {
1915 Ok(TimestampRange {
1916 start: arg_timestamp,
1917 end: self_timestamp,
1918 })
1919 });
1920 Ok(out_property.into_dyn_wrapped())
1921 },
1922 );
1923 map
1924}
1925
1926fn builtin_timestamp_range_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1927-> TemplateBuildMethodFnMap<'a, L, TimestampRange> {
1928 let mut map = TemplateBuildMethodFnMap::<L, TimestampRange>::new();
1931 map.insert(
1932 "start",
1933 |_language, _diagnostics, _build_ctx, self_property, function| {
1934 function.expect_no_arguments()?;
1935 let out_property = self_property.map(|time_range| time_range.start);
1936 Ok(out_property.into_dyn_wrapped())
1937 },
1938 );
1939 map.insert(
1940 "end",
1941 |_language, _diagnostics, _build_ctx, self_property, function| {
1942 function.expect_no_arguments()?;
1943 let out_property = self_property.map(|time_range| time_range.end);
1944 Ok(out_property.into_dyn_wrapped())
1945 },
1946 );
1947 map.insert(
1948 "duration",
1949 |_language, _diagnostics, _build_ctx, self_property, function| {
1950 function.expect_no_arguments()?;
1951 let out_property = self_property.and_then(|time_range| {
1953 let mut f = timeago::Formatter::new();
1954 f.min_unit(timeago::TimeUnit::Microseconds).ago("");
1955 let duration = time_util::format_duration(&time_range.start, &time_range.end, &f)?;
1956 if duration == "now" {
1957 Ok("less than a microsecond".to_owned())
1958 } else {
1959 Ok(duration)
1960 }
1961 });
1962 Ok(out_property.into_dyn_wrapped())
1963 },
1964 );
1965 map
1966}
1967
1968fn builtin_any_list_methods<'a, L: TemplateLanguage<'a> + ?Sized>() -> BuildAnyMethodFnMap<'a, L> {
1969 let mut map = BuildAnyMethodFnMap::<L>::new();
1972 map.insert(
1973 "join",
1974 |language, diagnostics, build_ctx, self_template, function| {
1975 let [separator_node] = function.expect_exact_arguments()?;
1976 let separator =
1977 expect_template_expression(language, diagnostics, build_ctx, separator_node)?;
1978 Ok(L::Property::wrap_template(
1979 self_template.try_join(separator).ok_or_else(|| {
1980 TemplateParseError::expected_type("Template", "AnyList", function.name_span)
1983 })?,
1984 ))
1985 },
1986 );
1987 map
1988}
1989
1990pub fn builtin_formattable_list_methods<'a, L, O>() -> TemplateBuildMethodFnMap<'a, L, Vec<O>>
1992where
1993 L: TemplateLanguage<'a> + ?Sized,
1994 L::Property: WrapTemplateProperty<'a, O> + WrapTemplateProperty<'a, Vec<O>>,
1995 O: Template + Clone + 'a,
1996{
1997 let mut map = builtin_unformattable_list_methods::<L, O>();
1998 map.insert(
1999 "join",
2000 |language, diagnostics, build_ctx, self_property, function| {
2001 let [separator_node] = function.expect_exact_arguments()?;
2002 let separator =
2003 expect_template_expression(language, diagnostics, build_ctx, separator_node)?;
2004 let template =
2005 ListPropertyTemplate::new(self_property, separator, |formatter, item| {
2006 item.format(formatter)
2007 });
2008 Ok(L::Property::wrap_template(Box::new(template)))
2009 },
2010 );
2011 map
2012}
2013
2014pub fn builtin_unformattable_list_methods<'a, L, O>() -> TemplateBuildMethodFnMap<'a, L, Vec<O>>
2016where
2017 L: TemplateLanguage<'a> + ?Sized,
2018 L::Property: WrapTemplateProperty<'a, O> + WrapTemplateProperty<'a, Vec<O>>,
2019 O: Clone + 'a,
2020{
2021 let mut map = TemplateBuildMethodFnMap::<L, Vec<O>>::new();
2024 map.insert(
2025 "len",
2026 |_language, _diagnostics, _build_ctx, self_property, function| {
2027 function.expect_no_arguments()?;
2028 let out_property = self_property.and_then(|items| Ok(i64::try_from(items.len())?));
2029 Ok(out_property.into_dyn_wrapped())
2030 },
2031 );
2032 map.insert(
2033 "filter",
2034 |language, diagnostics, build_ctx, self_property, function| {
2035 let out_property: BoxedTemplateProperty<'a, Vec<O>> =
2036 build_filter_operation(language, diagnostics, build_ctx, self_property, function)?;
2037 Ok(L::Property::wrap_property(out_property))
2038 },
2039 );
2040 map.insert(
2041 "map",
2042 |language, diagnostics, build_ctx, self_property, function| {
2043 let map_result =
2044 build_map_operation(language, diagnostics, build_ctx, self_property, function)?;
2045 Ok(L::Property::wrap_any_list(map_result))
2046 },
2047 );
2048 map.insert(
2049 "any",
2050 |language, diagnostics, build_ctx, self_property, function| {
2051 let out_property =
2052 build_any_operation(language, diagnostics, build_ctx, self_property, function)?;
2053 Ok(out_property.into_dyn_wrapped())
2054 },
2055 );
2056 map.insert(
2057 "all",
2058 |language, diagnostics, build_ctx, self_property, function| {
2059 let out_property =
2060 build_all_operation(language, diagnostics, build_ctx, self_property, function)?;
2061 Ok(out_property.into_dyn_wrapped())
2062 },
2063 );
2064 map.insert(
2065 "first",
2066 |_language, _diagnostics, _build_ctx, self_property, function| {
2067 function.expect_no_arguments()?;
2068 let out_property = self_property.and_then(|items| {
2070 items
2071 .into_iter()
2072 .next()
2073 .ok_or_else(|| TemplatePropertyError("List is empty".into()))
2074 });
2075 Ok(L::Property::wrap_property(out_property.into_dyn()))
2076 },
2077 );
2078 map.insert(
2079 "last",
2080 |_language, _diagnostics, _build_ctx, self_property, function| {
2081 function.expect_no_arguments()?;
2082 let out_property = self_property.and_then(|mut items| {
2084 items
2085 .pop()
2086 .ok_or_else(|| TemplatePropertyError("List is empty".into()))
2087 });
2088 Ok(L::Property::wrap_property(out_property.into_dyn()))
2089 },
2090 );
2091 map.insert(
2092 "get",
2093 |language, diagnostics, build_ctx, self_property, function| {
2094 let [index_node] = function.expect_exact_arguments()?;
2095 let index = expect_usize_expression(language, diagnostics, build_ctx, index_node)?;
2096 let out_property = (self_property, index).and_then(|(mut items, index)| {
2098 if index < items.len() {
2099 Ok(items.remove(index))
2100 } else {
2101 Err(TemplatePropertyError(
2102 format!("Index {index} out of bounds").into(),
2103 ))
2104 }
2105 });
2106 Ok(L::Property::wrap_property(out_property.into_dyn()))
2107 },
2108 );
2109 map.insert(
2110 "reverse",
2111 |_language, _diagnostics, _build_ctx, self_property, function| {
2112 function.expect_no_arguments()?;
2113 let out_property = self_property.map(|mut items| {
2114 items.reverse();
2115 items
2116 });
2117 Ok(L::Property::wrap_property(out_property.into_dyn()))
2118 },
2119 );
2120 map.insert(
2121 "skip",
2122 |language, diagnostics, build_ctx, self_property, function| {
2123 let [count_node] = function.expect_exact_arguments()?;
2124 let count = expect_usize_expression(language, diagnostics, build_ctx, count_node)?;
2125 let out_property = (self_property, count)
2126 .map(|(items, count)| items.into_iter().skip(count).collect_vec());
2127 Ok(L::Property::wrap_property(out_property.into_dyn()))
2128 },
2129 );
2130 map.insert(
2131 "take",
2132 |language, diagnostics, build_ctx, self_property, function| {
2133 let [count_node] = function.expect_exact_arguments()?;
2134 let count = expect_usize_expression(language, diagnostics, build_ctx, count_node)?;
2135 let out_property = (self_property, count)
2136 .map(|(items, count)| items.into_iter().take(count).collect_vec());
2137 Ok(L::Property::wrap_property(out_property.into_dyn()))
2138 },
2139 );
2140 map
2141}
2142
2143fn build_filter_operation<'a, L, O, P, B>(
2145 language: &L,
2146 diagnostics: &mut TemplateDiagnostics,
2147 build_ctx: &BuildContext<L::Property>,
2148 self_property: P,
2149 function: &FunctionCallNode,
2150) -> TemplateParseResult<BoxedTemplateProperty<'a, B>>
2151where
2152 L: TemplateLanguage<'a> + ?Sized,
2153 L::Property: WrapTemplateProperty<'a, O>,
2154 P: TemplateProperty + 'a,
2155 P::Output: IntoIterator<Item = O>,
2156 O: Clone + 'a,
2157 B: FromIterator<O>,
2158{
2159 let [lambda_node] = function.expect_exact_arguments()?;
2160 let item_placeholder = PropertyPlaceholder::new();
2161 let item_predicate =
2162 template_parser::catch_aliases(diagnostics, lambda_node, |diagnostics, node| {
2163 let lambda = template_parser::expect_lambda(node)?;
2164 build_lambda_expression(
2165 build_ctx,
2166 lambda,
2167 &[&|| item_placeholder.clone().into_dyn_wrapped()],
2168 |build_ctx, body| expect_boolean_expression(language, diagnostics, build_ctx, body),
2169 )
2170 })?;
2171 let out_property = self_property.and_then(move |items| {
2172 items
2173 .into_iter()
2174 .filter_map(|item| {
2175 item_placeholder.set(item);
2177 let result = item_predicate.extract();
2178 let item = item_placeholder.take().unwrap();
2179 result.map(|pred| pred.then_some(item)).transpose()
2180 })
2181 .collect()
2182 });
2183 Ok(out_property.into_dyn())
2184}
2185
2186fn build_map_operation<'a, L, O, P>(
2189 language: &L,
2190 diagnostics: &mut TemplateDiagnostics,
2191 build_ctx: &BuildContext<L::Property>,
2192 self_property: P,
2193 function: &FunctionCallNode,
2194) -> TemplateParseResult<BoxedAnyProperty<'a>>
2195where
2196 L: TemplateLanguage<'a> + ?Sized,
2197 L::Property: WrapTemplateProperty<'a, O>,
2198 P: TemplateProperty + 'a,
2199 P::Output: IntoIterator<Item = O>,
2200 O: Clone + 'a,
2201{
2202 let [lambda_node] = function.expect_exact_arguments()?;
2203 let item_placeholder = PropertyPlaceholder::new();
2204 let mapped_item =
2205 template_parser::catch_aliases(diagnostics, lambda_node, |diagnostics, node| {
2206 let lambda = template_parser::expect_lambda(node)?;
2207 build_lambda_expression(
2208 build_ctx,
2209 lambda,
2210 &[&|| item_placeholder.clone().into_dyn_wrapped()],
2211 |build_ctx, body| expect_any_expression(language, diagnostics, build_ctx, body),
2212 )
2213 })?;
2214 let mapped_list = ListMapProperty::new(self_property, item_placeholder, mapped_item);
2215 Ok(Box::new(mapped_list))
2216}
2217
2218fn build_any_operation<'a, L, O, P>(
2221 language: &L,
2222 diagnostics: &mut TemplateDiagnostics,
2223 build_ctx: &BuildContext<L::Property>,
2224 self_property: P,
2225 function: &FunctionCallNode,
2226) -> TemplateParseResult<BoxedTemplateProperty<'a, bool>>
2227where
2228 L: TemplateLanguage<'a> + ?Sized,
2229 L::Property: WrapTemplateProperty<'a, O>,
2230 P: TemplateProperty + 'a,
2231 P::Output: IntoIterator<Item = O>,
2232 O: Clone + 'a,
2233{
2234 let [lambda_node] = function.expect_exact_arguments()?;
2235 let item_placeholder = PropertyPlaceholder::new();
2236 let item_predicate =
2237 template_parser::catch_aliases(diagnostics, lambda_node, |diagnostics, node| {
2238 let lambda = template_parser::expect_lambda(node)?;
2239 build_lambda_expression(
2240 build_ctx,
2241 lambda,
2242 &[&|| item_placeholder.clone().into_dyn_wrapped()],
2243 |build_ctx, body| expect_boolean_expression(language, diagnostics, build_ctx, body),
2244 )
2245 })?;
2246
2247 let out_property = self_property.and_then(move |items| {
2248 items
2249 .into_iter()
2250 .map(|item| item_placeholder.with_value(item, || item_predicate.extract()))
2251 .process_results(|mut predicates| predicates.any(|p| p))
2252 });
2253 Ok(out_property.into_dyn())
2254}
2255
2256fn build_all_operation<'a, L, O, P>(
2259 language: &L,
2260 diagnostics: &mut TemplateDiagnostics,
2261 build_ctx: &BuildContext<L::Property>,
2262 self_property: P,
2263 function: &FunctionCallNode,
2264) -> TemplateParseResult<BoxedTemplateProperty<'a, bool>>
2265where
2266 L: TemplateLanguage<'a> + ?Sized,
2267 L::Property: WrapTemplateProperty<'a, O>,
2268 P: TemplateProperty + 'a,
2269 P::Output: IntoIterator<Item = O>,
2270 O: Clone + 'a,
2271{
2272 let [lambda_node] = function.expect_exact_arguments()?;
2273 let item_placeholder = PropertyPlaceholder::new();
2274 let item_predicate =
2275 template_parser::catch_aliases(diagnostics, lambda_node, |diagnostics, node| {
2276 let lambda = template_parser::expect_lambda(node)?;
2277 build_lambda_expression(
2278 build_ctx,
2279 lambda,
2280 &[&|| item_placeholder.clone().into_dyn_wrapped()],
2281 |build_ctx, body| expect_boolean_expression(language, diagnostics, build_ctx, body),
2282 )
2283 })?;
2284
2285 let out_property = self_property.and_then(move |items| {
2286 items
2287 .into_iter()
2288 .map(|item| item_placeholder.with_value(item, || item_predicate.extract()))
2289 .process_results(|mut predicates| predicates.all(|p| p))
2290 });
2291 Ok(out_property.into_dyn())
2292}
2293
2294fn build_lambda_expression<'i, P, T>(
2297 build_ctx: &BuildContext<'i, P>,
2298 lambda: &LambdaNode<'i>,
2299 arg_fns: &[&'i dyn Fn() -> P],
2300 build_body: impl FnOnce(&BuildContext<'i, P>, &ExpressionNode<'i>) -> TemplateParseResult<T>,
2301) -> TemplateParseResult<T> {
2302 if lambda.params.len() != arg_fns.len() {
2303 return Err(TemplateParseError::expression(
2304 format!("Expected {} lambda parameters", arg_fns.len()),
2305 lambda.params_span,
2306 ));
2307 }
2308 let mut local_variables = build_ctx.local_variables.clone();
2309 local_variables.extend(iter::zip(&lambda.params, arg_fns));
2310 let inner_build_ctx = BuildContext {
2311 local_variables,
2312 self_variable: build_ctx.self_variable,
2313 };
2314 build_body(&inner_build_ctx, &lambda.body)
2315}
2316
2317fn builtin_functions<'a, L: TemplateLanguage<'a> + ?Sized>() -> TemplateBuildFunctionFnMap<'a, L> {
2318 let mut map = TemplateBuildFunctionFnMap::<L>::new();
2321 map.insert("fill", |language, diagnostics, build_ctx, function| {
2322 let [width_node, content_node] = function.expect_exact_arguments()?;
2323 let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
2324 let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
2325 let template =
2326 ReformatTemplate::new(content, move |formatter, recorded| match width.extract() {
2327 Ok(width) => text_util::write_wrapped(formatter.as_mut(), recorded, width),
2328 Err(err) => formatter.handle_error(err),
2329 });
2330 Ok(L::Property::wrap_template(Box::new(template)))
2331 });
2332 map.insert("indent", |language, diagnostics, build_ctx, function| {
2333 let [prefix_node, content_node] = function.expect_exact_arguments()?;
2334 let prefix = expect_template_expression(language, diagnostics, build_ctx, prefix_node)?;
2335 let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
2336 let template = ReformatTemplate::new(content, move |formatter, recorded| {
2337 let rewrap = formatter.rewrap_fn();
2338 text_util::write_indented(formatter.as_mut(), recorded, |formatter| {
2339 prefix.format(&mut rewrap(formatter))
2340 })
2341 });
2342 Ok(L::Property::wrap_template(Box::new(template)))
2343 });
2344 map.insert("pad_start", |language, diagnostics, build_ctx, function| {
2345 let ([width_node, content_node], [fill_char_node]) =
2346 function.expect_named_arguments(&["", "", "fill_char"])?;
2347 let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
2348 let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
2349 let fill_char = fill_char_node
2350 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
2351 .transpose()?;
2352 let template = new_pad_template(content, fill_char, width, text_util::write_padded_start);
2353 Ok(L::Property::wrap_template(template))
2354 });
2355 map.insert("pad_end", |language, diagnostics, build_ctx, function| {
2356 let ([width_node, content_node], [fill_char_node]) =
2357 function.expect_named_arguments(&["", "", "fill_char"])?;
2358 let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
2359 let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
2360 let fill_char = fill_char_node
2361 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
2362 .transpose()?;
2363 let template = new_pad_template(content, fill_char, width, text_util::write_padded_end);
2364 Ok(L::Property::wrap_template(template))
2365 });
2366 map.insert(
2367 "pad_centered",
2368 |language, diagnostics, build_ctx, function| {
2369 let ([width_node, content_node], [fill_char_node]) =
2370 function.expect_named_arguments(&["", "", "fill_char"])?;
2371 let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
2372 let content =
2373 expect_template_expression(language, diagnostics, build_ctx, content_node)?;
2374 let fill_char = fill_char_node
2375 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
2376 .transpose()?;
2377 let template =
2378 new_pad_template(content, fill_char, width, text_util::write_padded_centered);
2379 Ok(L::Property::wrap_template(template))
2380 },
2381 );
2382 map.insert(
2383 "truncate_start",
2384 |language, diagnostics, build_ctx, function| {
2385 let ([width_node, content_node], [ellipsis_node]) =
2386 function.expect_named_arguments(&["", "", "ellipsis"])?;
2387 let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
2388 let content =
2389 expect_template_expression(language, diagnostics, build_ctx, content_node)?;
2390 let ellipsis = ellipsis_node
2391 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
2392 .transpose()?;
2393 let template =
2394 new_truncate_template(content, ellipsis, width, text_util::write_truncated_start);
2395 Ok(L::Property::wrap_template(template))
2396 },
2397 );
2398 map.insert(
2399 "truncate_end",
2400 |language, diagnostics, build_ctx, function| {
2401 let ([width_node, content_node], [ellipsis_node]) =
2402 function.expect_named_arguments(&["", "", "ellipsis"])?;
2403 let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
2404 let content =
2405 expect_template_expression(language, diagnostics, build_ctx, content_node)?;
2406 let ellipsis = ellipsis_node
2407 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
2408 .transpose()?;
2409 let template =
2410 new_truncate_template(content, ellipsis, width, text_util::write_truncated_end);
2411 Ok(L::Property::wrap_template(template))
2412 },
2413 );
2414 map.insert("hash", |language, diagnostics, build_ctx, function| {
2415 let [content_node] = function.expect_exact_arguments()?;
2416 let content = expect_stringify_expression(language, diagnostics, build_ctx, content_node)?;
2417 let result = content.map(|c| hex_util::encode_hex(blake2b_hash(&c).as_ref()));
2418 Ok(result.into_dyn_wrapped())
2419 });
2420 map.insert("label", |language, diagnostics, build_ctx, function| {
2421 let [label_node, content_node] = function.expect_exact_arguments()?;
2422 let label_property =
2423 expect_stringify_expression(language, diagnostics, build_ctx, label_node)?;
2424 let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
2425 let labels =
2426 label_property.map(|s| s.split_whitespace().map(ToString::to_string).collect());
2427 Ok(L::Property::wrap_template(Box::new(LabelTemplate::new(
2428 content, labels,
2429 ))))
2430 });
2431 map.insert(
2432 "raw_escape_sequence",
2433 |language, diagnostics, build_ctx, function| {
2434 let [content_node] = function.expect_exact_arguments()?;
2435 let content =
2436 expect_template_expression(language, diagnostics, build_ctx, content_node)?;
2437 Ok(L::Property::wrap_template(Box::new(
2438 RawEscapeSequenceTemplate(content),
2439 )))
2440 },
2441 );
2442 map.insert("hyperlink", |language, diagnostics, build_ctx, function| {
2443 let ([url_node, text_node], [fallback_node]) = function.expect_arguments()?;
2444 let url = expect_stringify_expression(language, diagnostics, build_ctx, url_node)?;
2445 let text = expect_template_expression(language, diagnostics, build_ctx, text_node)?;
2446 let fallback = fallback_node
2447 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
2448 .transpose()?;
2449 Ok(L::Property::wrap_template(Box::new(
2450 HyperlinkTemplate::new(url, text, fallback),
2451 )))
2452 });
2453 map.insert("replace", |language, diagnostics, build_ctx, function| {
2454 let ([pattern_node, content_node, replacement_lambda_node], []) =
2455 function.expect_arguments()?;
2456 let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
2457 let pattern = template_parser::expect_string_pattern(pattern_node)?;
2458
2459 let regex = pattern.to_regex();
2460
2461 let captures_placeholder = PropertyPlaceholder::new();
2462 let replacement_template = template_parser::catch_aliases(
2463 diagnostics,
2464 replacement_lambda_node,
2465 |diagnostics, node| {
2466 let lambda = template_parser::expect_lambda(node)?;
2467 build_lambda_expression(
2468 build_ctx,
2469 lambda,
2470 &[&|| captures_placeholder.clone().into_dyn_wrapped()],
2471 |build_ctx, body| {
2472 expect_template_expression(language, diagnostics, build_ctx, body)
2473 },
2474 )
2475 },
2476 )?;
2477
2478 let template = new_replace_template(
2479 content,
2480 regex.clone(),
2481 captures_placeholder.clone(),
2482 move |formatter| replacement_template.format(formatter),
2483 );
2484 Ok(L::Property::wrap_template(template))
2485 });
2486 map.insert("stringify", |language, diagnostics, build_ctx, function| {
2487 let [content_node] = function.expect_exact_arguments()?;
2488 let content = expect_stringify_expression(language, diagnostics, build_ctx, content_node)?;
2489 Ok(L::Property::wrap_property(content))
2490 });
2491 map.insert("json", |language, diagnostics, build_ctx, function| {
2492 let [value_node] = function.expect_exact_arguments()?;
2496 let value = expect_serialize_expression(language, diagnostics, build_ctx, value_node)?;
2497 let out_property = value.and_then(|v| Ok(serde_json::to_string(&v)?));
2498 Ok(out_property.into_dyn_wrapped())
2499 });
2500 map.insert("if", |language, diagnostics, build_ctx, function| {
2501 let ([condition_node, true_node], [false_node]) = function.expect_arguments()?;
2502 let condition =
2503 expect_boolean_expression(language, diagnostics, build_ctx, condition_node)?;
2504 let true_any = expect_any_expression(language, diagnostics, build_ctx, true_node)?;
2505 let false_any = false_node
2506 .map(|node| expect_any_expression(language, diagnostics, build_ctx, node))
2507 .transpose()?;
2508 let property = ConditionalProperty::new(condition, true_any, false_any);
2509 Ok(L::Property::wrap_any(Box::new(property)))
2510 });
2511 map.insert("coalesce", |language, diagnostics, build_ctx, function| {
2512 let ([], content_nodes) = function.expect_some_arguments()?;
2513 let contents = content_nodes
2514 .iter()
2515 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
2516 .try_collect()?;
2517 Ok(L::Property::wrap_template(Box::new(CoalesceTemplate(
2518 contents,
2519 ))))
2520 });
2521 map.insert("concat", |language, diagnostics, build_ctx, function| {
2522 let ([], content_nodes) = function.expect_some_arguments()?;
2523 let contents = content_nodes
2524 .iter()
2525 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
2526 .try_collect()?;
2527 Ok(L::Property::wrap_template(Box::new(ConcatTemplate(
2528 contents,
2529 ))))
2530 });
2531 map.insert("join", |language, diagnostics, build_ctx, function| {
2532 let ([separator_node], content_nodes) = function.expect_some_arguments()?;
2533 let separator =
2534 expect_template_expression(language, diagnostics, build_ctx, separator_node)?;
2535 let contents = content_nodes
2536 .iter()
2537 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
2538 .try_collect()?;
2539 Ok(L::Property::wrap_template(Box::new(JoinTemplate::new(
2540 separator, contents,
2541 ))))
2542 });
2543 map.insert("separate", |language, diagnostics, build_ctx, function| {
2544 let ([separator_node], content_nodes) = function.expect_some_arguments()?;
2545 let separator =
2546 expect_template_expression(language, diagnostics, build_ctx, separator_node)?;
2547 let contents = content_nodes
2548 .iter()
2549 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
2550 .try_collect()?;
2551 Ok(L::Property::wrap_template(Box::new(SeparateTemplate::new(
2552 separator, contents,
2553 ))))
2554 });
2555 map.insert("surround", |language, diagnostics, build_ctx, function| {
2556 let [prefix_node, suffix_node, content_node] = function.expect_exact_arguments()?;
2557 let prefix = expect_template_expression(language, diagnostics, build_ctx, prefix_node)?;
2558 let suffix = expect_template_expression(language, diagnostics, build_ctx, suffix_node)?;
2559 let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
2560 let template = ReformatTemplate::new(content, move |formatter, recorded| {
2561 if recorded.data().is_empty() {
2562 return Ok(());
2563 }
2564 prefix.format(formatter)?;
2565 recorded.replay(formatter.as_mut())?;
2566 suffix.format(formatter)?;
2567 Ok(())
2568 });
2569 Ok(L::Property::wrap_template(Box::new(template)))
2570 });
2571 map.insert("config", |language, diagnostics, build_ctx, function| {
2572 let [name_node] = function.expect_exact_arguments()?;
2573 let name_expression =
2574 expect_stringify_expression(language, diagnostics, build_ctx, name_node)?;
2575 if let Ok(name) = name_expression.extract() {
2576 let config_path: ConfigNamePathBuf =
2577 template_parser::catch_aliases(diagnostics, name_node, |_diagnostics, node| {
2578 name.parse().map_err(|err| {
2579 TemplateParseError::expression("Failed to parse config name", node.span)
2580 .with_source(err)
2581 })
2582 })?;
2583 let value = language
2584 .settings()
2585 .get_value(config_path)
2586 .optional()
2587 .map_err(|err| {
2588 TemplateParseError::expression("Failed to get config value", function.name_span)
2589 .with_source(err)
2590 })?;
2591 Ok(Literal(value.map(|v| v.decorated("", ""))).into_dyn_wrapped())
2593 } else {
2594 let settings = language.settings().clone();
2595 let out_property = name_expression.and_then(move |name| {
2596 let config_path: ConfigNamePathBuf = name.parse()?;
2597 let value = settings.get_value(config_path).optional()?;
2598 Ok(value.map(|v| v.decorated("", "")))
2599 });
2600 Ok(out_property.into_dyn_wrapped())
2601 }
2602 });
2603 map
2604}
2605
2606fn new_pad_template<'a, W>(
2607 content: Box<dyn Template + 'a>,
2608 fill_char: Option<Box<dyn Template + 'a>>,
2609 width: BoxedTemplateProperty<'a, usize>,
2610 write_padded: W,
2611) -> Box<dyn Template + 'a>
2612where
2613 W: Fn(&mut dyn Formatter, &FormatRecorder, &FormatRecorder, usize) -> io::Result<()> + 'a,
2614{
2615 let default_fill_char = FormatRecorder::with_data(" ");
2616 let template = ReformatTemplate::new(content, move |formatter, recorded| {
2617 let width = match width.extract() {
2618 Ok(width) => width,
2619 Err(err) => return formatter.handle_error(err),
2620 };
2621 let mut fill_char_recorder;
2622 let recorded_fill_char = if let Some(fill_char) = &fill_char {
2623 let rewrap = formatter.rewrap_fn();
2624 fill_char_recorder = FormatRecorder::new(formatter.maybe_color());
2625 fill_char.format(&mut rewrap(&mut fill_char_recorder))?;
2626 &fill_char_recorder
2627 } else {
2628 &default_fill_char
2629 };
2630 write_padded(formatter.as_mut(), recorded, recorded_fill_char, width)
2631 });
2632 Box::new(template)
2633}
2634
2635fn new_truncate_template<'a, W>(
2636 content: Box<dyn Template + 'a>,
2637 ellipsis: Option<Box<dyn Template + 'a>>,
2638 width: BoxedTemplateProperty<'a, usize>,
2639 write_truncated: W,
2640) -> Box<dyn Template + 'a>
2641where
2642 W: Fn(&mut dyn Formatter, &FormatRecorder, &FormatRecorder, usize) -> io::Result<usize> + 'a,
2643{
2644 let default_ellipsis = FormatRecorder::with_data("");
2645 let template = ReformatTemplate::new(content, move |formatter, recorded| {
2646 let width = match width.extract() {
2647 Ok(width) => width,
2648 Err(err) => return formatter.handle_error(err),
2649 };
2650 let mut ellipsis_recorder;
2651 let recorded_ellipsis = if let Some(ellipsis) = &ellipsis {
2652 let rewrap = formatter.rewrap_fn();
2653 ellipsis_recorder = FormatRecorder::new(formatter.maybe_color());
2654 ellipsis.format(&mut rewrap(&mut ellipsis_recorder))?;
2655 &ellipsis_recorder
2656 } else {
2657 &default_ellipsis
2658 };
2659 write_truncated(formatter.as_mut(), recorded, recorded_ellipsis, width)?;
2660 Ok(())
2661 });
2662 Box::new(template)
2663}
2664
2665fn new_replace_template<'a, W>(
2666 content: Box<dyn Template + 'a>,
2667 regex: regex::bytes::Regex,
2668 captures_placeholder: PropertyPlaceholder<RegexCaptures>,
2669 write_replacement_content: W,
2670) -> Box<dyn Template + 'a>
2671where
2672 W: Fn(&mut TemplateFormatter) -> io::Result<()> + 'a,
2673{
2674 let names_map: HashMap<String, usize> = regex
2676 .capture_names()
2677 .enumerate()
2678 .filter_map(|(i, name)| name.map(|n| (n.to_string(), i)))
2679 .collect();
2680
2681 let template = ReformatTemplate::new(content, move |formatter, recorded| {
2682 let data = recorded.data();
2683
2684 let mut recorded_replacements = vec![];
2685 let mut content_ranges = vec![];
2686
2687 for captures in regex.captures_iter(data) {
2688 let full_match = captures.get(0).expect("capture group 0 is always present");
2689 content_ranges.push(full_match.range());
2690
2691 let capture_ranges = captures
2692 .iter()
2693 .map(|m| m.map(|m| m.range()).unwrap_or_default())
2694 .collect_vec();
2695
2696 captures_placeholder.with_value(
2697 RegexCaptures::new(data.to_vec(), capture_ranges, names_map.clone()),
2698 || -> io::Result<()> {
2699 let mut recorder = FormatRecorder::new(formatter.maybe_color());
2700 let rewrap = formatter.rewrap_fn();
2701 write_replacement_content(&mut rewrap(&mut recorder))?;
2702 recorded_replacements.push(recorder);
2703 Ok(())
2704 },
2705 )?;
2706 }
2707
2708 write_replaced(
2709 formatter.as_mut(),
2710 recorded,
2711 &content_ranges,
2712 |formatter, index| recorded_replacements[index].replay(formatter),
2713 )
2714 });
2715
2716 Box::new(template)
2717}
2718
2719pub fn build_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2721 language: &L,
2722 diagnostics: &mut TemplateDiagnostics,
2723 build_ctx: &BuildContext<L::Property>,
2724 node: &ExpressionNode,
2725) -> TemplateParseResult<Expression<L::Property>> {
2726 template_parser::catch_aliases(diagnostics, node, |diagnostics, node| match &node.kind {
2727 ExpressionKind::Identifier(name) => {
2728 if let Some(make) = build_ctx.local_variables.get(name) {
2729 Ok(Expression::unlabeled(make()))
2731 } else if *name == "self" {
2732 let make = build_ctx.self_variable;
2734 Ok(Expression::unlabeled(make()))
2735 } else {
2736 let property = build_keyword(language, diagnostics, build_ctx, name, node.span)
2737 .map_err(|err| {
2738 err.extend_keyword_candidates(itertools::chain(
2739 build_ctx.local_variables.keys().copied(),
2740 ["self"],
2741 ))
2742 })?;
2743 Ok(Expression::with_label(property, *name))
2744 }
2745 }
2746 ExpressionKind::Boolean(value) => {
2747 let property = Literal(*value).into_dyn_wrapped();
2748 Ok(Expression::unlabeled(property))
2749 }
2750 ExpressionKind::Integer(value) => {
2751 let property = Literal(*value).into_dyn_wrapped();
2752 Ok(Expression::unlabeled(property))
2753 }
2754 ExpressionKind::String(value) => {
2755 let property = Literal(value.clone()).into_dyn_wrapped();
2756 Ok(Expression::unlabeled(property))
2757 }
2758 ExpressionKind::Pattern(_) => Err(TemplateParseError::expression(
2759 "String patterns may not be used as expression values",
2760 node.span,
2761 )),
2762 ExpressionKind::Unary(op, arg_node) => {
2763 let property = build_unary_operation(language, diagnostics, build_ctx, *op, arg_node)?;
2764 Ok(Expression::unlabeled(property))
2765 }
2766 ExpressionKind::Binary(op, lhs_node, rhs_node) => {
2767 let property = build_binary_operation(
2768 language,
2769 diagnostics,
2770 build_ctx,
2771 *op,
2772 lhs_node,
2773 rhs_node,
2774 node.span,
2775 )?;
2776 Ok(Expression::unlabeled(property))
2777 }
2778 ExpressionKind::Concat(nodes) => {
2779 let templates = nodes
2780 .iter()
2781 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
2782 .try_collect()?;
2783 let property = L::Property::wrap_template(Box::new(ConcatTemplate(templates)));
2784 Ok(Expression::unlabeled(property))
2785 }
2786 ExpressionKind::FunctionCall(function) => {
2787 let property = language.build_function(diagnostics, build_ctx, function)?;
2788 Ok(Expression::unlabeled(property))
2789 }
2790 ExpressionKind::MethodCall(method) => {
2791 let mut expression =
2792 build_expression(language, diagnostics, build_ctx, &method.object)?;
2793 expression.property = language.build_method(
2794 diagnostics,
2795 build_ctx,
2796 expression.property,
2797 &method.function,
2798 )?;
2799 expression.labels.push(method.function.name.to_owned());
2800 Ok(expression)
2801 }
2802 ExpressionKind::Lambda(_) => Err(TemplateParseError::expression(
2803 "Lambda cannot be defined here",
2804 node.span,
2805 )),
2806 ExpressionKind::AliasExpanded(..) => unreachable!(),
2807 })
2808}
2809
2810pub fn build<'a, C, L>(
2812 language: &L,
2813 diagnostics: &mut TemplateDiagnostics,
2814 node: &ExpressionNode,
2815) -> TemplateParseResult<TemplateRenderer<'a, C>>
2816where
2817 C: Clone + 'a,
2818 L: TemplateLanguage<'a> + ?Sized,
2819 L::Property: WrapTemplateProperty<'a, C>,
2820{
2821 let self_placeholder = PropertyPlaceholder::new();
2822 let build_ctx = BuildContext {
2823 local_variables: HashMap::new(),
2824 self_variable: &|| self_placeholder.clone().into_dyn_wrapped(),
2825 };
2826 let template = expect_template_expression(language, diagnostics, &build_ctx, node)?;
2827 Ok(TemplateRenderer::new(template, self_placeholder))
2828}
2829
2830pub fn parse<'a, C, L>(
2832 language: &L,
2833 diagnostics: &mut TemplateDiagnostics,
2834 template_text: &str,
2835 aliases_map: &TemplateAliasesMap,
2836) -> TemplateParseResult<TemplateRenderer<'a, C>>
2837where
2838 C: Clone + 'a,
2839 L: TemplateLanguage<'a> + ?Sized,
2840 L::Property: WrapTemplateProperty<'a, C>,
2841{
2842 let node = template_parser::parse(template_text, aliases_map)?;
2843 build(language, diagnostics, &node).map_err(|err| err.extend_alias_candidates(aliases_map))
2844}
2845
2846pub fn expect_boolean_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2847 language: &L,
2848 diagnostics: &mut TemplateDiagnostics,
2849 build_ctx: &BuildContext<L::Property>,
2850 node: &ExpressionNode,
2851) -> TemplateParseResult<BoxedTemplateProperty<'a, bool>> {
2852 expect_expression_of_type(
2853 language,
2854 diagnostics,
2855 build_ctx,
2856 node,
2857 "Boolean",
2858 |expression| expression.try_into_boolean(),
2859 )
2860}
2861
2862pub fn expect_integer_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2863 language: &L,
2864 diagnostics: &mut TemplateDiagnostics,
2865 build_ctx: &BuildContext<L::Property>,
2866 node: &ExpressionNode,
2867) -> TemplateParseResult<BoxedTemplateProperty<'a, i64>> {
2868 expect_expression_of_type(
2869 language,
2870 diagnostics,
2871 build_ctx,
2872 node,
2873 "Integer",
2874 |expression| expression.try_into_integer(),
2875 )
2876}
2877
2878pub fn expect_isize_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2880 language: &L,
2881 diagnostics: &mut TemplateDiagnostics,
2882 build_ctx: &BuildContext<L::Property>,
2883 node: &ExpressionNode,
2884) -> TemplateParseResult<BoxedTemplateProperty<'a, isize>> {
2885 let i64_property = expect_integer_expression(language, diagnostics, build_ctx, node)?;
2886 let isize_property = i64_property.and_then(|v| Ok(isize::try_from(v)?));
2887 Ok(isize_property.into_dyn())
2888}
2889
2890pub fn expect_usize_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2892 language: &L,
2893 diagnostics: &mut TemplateDiagnostics,
2894 build_ctx: &BuildContext<L::Property>,
2895 node: &ExpressionNode,
2896) -> TemplateParseResult<BoxedTemplateProperty<'a, usize>> {
2897 let i64_property = expect_integer_expression(language, diagnostics, build_ctx, node)?;
2898 let usize_property = i64_property.and_then(|v| Ok(usize::try_from(v)?));
2899 Ok(usize_property.into_dyn())
2900}
2901
2902pub fn expect_byte_stringify_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2903 language: &L,
2904 diagnostics: &mut TemplateDiagnostics,
2905 build_ctx: &BuildContext<L::Property>,
2906 node: &ExpressionNode,
2907) -> TemplateParseResult<BoxedTemplateProperty<'a, BString>> {
2908 expect_expression_of_type(
2911 language,
2912 diagnostics,
2913 build_ctx,
2914 node,
2915 "ByteStringify",
2916 |expression| expression.try_into_byte_stringify(),
2917 )
2918}
2919
2920pub fn expect_stringify_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2921 language: &L,
2922 diagnostics: &mut TemplateDiagnostics,
2923 build_ctx: &BuildContext<L::Property>,
2924 node: &ExpressionNode,
2925) -> TemplateParseResult<BoxedTemplateProperty<'a, String>> {
2926 expect_expression_of_type(
2929 language,
2930 diagnostics,
2931 build_ctx,
2932 node,
2933 "Stringify",
2934 |expression| expression.try_into_stringify(),
2935 )
2936}
2937
2938pub fn expect_timestamp_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2939 language: &L,
2940 diagnostics: &mut TemplateDiagnostics,
2941 build_ctx: &BuildContext<L::Property>,
2942 node: &ExpressionNode,
2943) -> TemplateParseResult<BoxedTemplateProperty<'a, Timestamp>> {
2944 expect_expression_of_type(
2945 language,
2946 diagnostics,
2947 build_ctx,
2948 node,
2949 "Timestamp",
2950 |expression| expression.try_into_timestamp(),
2951 )
2952}
2953
2954pub fn expect_serialize_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2955 language: &L,
2956 diagnostics: &mut TemplateDiagnostics,
2957 build_ctx: &BuildContext<L::Property>,
2958 node: &ExpressionNode,
2959) -> TemplateParseResult<BoxedSerializeProperty<'a>> {
2960 expect_expression_of_type(
2961 language,
2962 diagnostics,
2963 build_ctx,
2964 node,
2965 "Serialize",
2966 |expression| expression.try_into_serialize(),
2967 )
2968}
2969
2970pub fn expect_template_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2971 language: &L,
2972 diagnostics: &mut TemplateDiagnostics,
2973 build_ctx: &BuildContext<L::Property>,
2974 node: &ExpressionNode,
2975) -> TemplateParseResult<Box<dyn Template + 'a>> {
2976 expect_expression_of_type(
2977 language,
2978 diagnostics,
2979 build_ctx,
2980 node,
2981 "Template",
2982 |expression| expression.try_into_template(),
2983 )
2984}
2985
2986pub fn expect_any_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2987 language: &L,
2988 diagnostics: &mut TemplateDiagnostics,
2989 build_ctx: &BuildContext<L::Property>,
2990 node: &ExpressionNode,
2991) -> TemplateParseResult<BoxedAnyProperty<'a>> {
2992 template_parser::catch_aliases(diagnostics, node, |diagnostics, node| {
2993 Ok(
2994 Box::new(build_expression(language, diagnostics, build_ctx, node)?)
2995 as BoxedAnyProperty<'a>,
2996 )
2997 })
2998}
2999
3000fn expect_expression_of_type<'a, L: TemplateLanguage<'a> + ?Sized, T>(
3001 language: &L,
3002 diagnostics: &mut TemplateDiagnostics,
3003 build_ctx: &BuildContext<L::Property>,
3004 node: &ExpressionNode,
3005 expected_type: &str,
3006 f: impl FnOnce(Expression<L::Property>) -> Option<T>,
3007) -> TemplateParseResult<T> {
3008 template_parser::catch_aliases(diagnostics, node, |diagnostics, node| {
3009 let expression = build_expression(language, diagnostics, build_ctx, node)?;
3010 let actual_type = expression.type_name();
3011 f(expression)
3012 .ok_or_else(|| TemplateParseError::expected_type(expected_type, actual_type, node.span))
3013 })
3014}
3015
3016#[cfg(test)]
3017mod tests {
3018 use assert_matches::assert_matches;
3019 use jj_lib::backend::MillisSinceEpoch;
3020 use jj_lib::config::StackedConfig;
3021
3022 use super::*;
3023 use crate::formatter;
3024 use crate::formatter::ColorFormatter;
3025 use crate::generic_templater;
3026 use crate::generic_templater::GenericTemplateLanguage;
3027
3028 #[derive(Clone, Debug, serde::Serialize)]
3029 struct Context;
3030
3031 type TestTemplateLanguage = GenericTemplateLanguage<'static, Context>;
3032 type TestTemplatePropertyKind = <TestTemplateLanguage as TemplateLanguage<'static>>::Property;
3033
3034 generic_templater::impl_self_property_wrapper!(Context);
3035
3036 struct TestTemplateEnv {
3038 language: TestTemplateLanguage,
3039 aliases_map: TemplateAliasesMap,
3040 color_rules: Vec<(Vec<String>, formatter::Style)>,
3041 }
3042
3043 impl TestTemplateEnv {
3044 fn new() -> Self {
3045 Self::with_config(StackedConfig::with_defaults())
3046 }
3047
3048 fn with_config(config: StackedConfig) -> Self {
3049 let settings = UserSettings::from_config(config).unwrap();
3050 Self {
3051 language: TestTemplateLanguage::new(&settings),
3052 aliases_map: TemplateAliasesMap::new(),
3053 color_rules: Vec::new(),
3054 }
3055 }
3056 }
3057
3058 impl TestTemplateEnv {
3059 fn add_keyword<F>(&mut self, name: &'static str, build: F)
3060 where
3061 F: Fn() -> TestTemplatePropertyKind + 'static,
3062 {
3063 self.language.add_keyword(name, move |_| Ok(build()));
3064 }
3065
3066 fn add_dynamic_keyword<O, F>(&mut self, name: &'static str, build: F)
3069 where
3070 O: 'static,
3071 F: Fn() -> O + Clone + 'static,
3072 TestTemplatePropertyKind: WrapTemplateProperty<'static, O>,
3073 {
3074 self.language.add_keyword(name, move |self_property| {
3075 let build = build.clone();
3076 Ok(self_property.map(move |_| build()).into_dyn_wrapped())
3077 });
3078 }
3079
3080 fn add_alias(&mut self, decl: impl AsRef<str>, defn: impl Into<String>) {
3081 self.aliases_map.insert(decl, defn).unwrap();
3082 }
3083
3084 fn add_color(&mut self, label: &str, fg: crossterm::style::Color) {
3085 let labels = label.split_whitespace().map(|s| s.to_owned()).collect();
3086 let style = formatter::Style {
3087 fg: Some(fg),
3088 ..Default::default()
3089 };
3090 self.color_rules.push((labels, style));
3091 }
3092
3093 fn parse(&self, template: &str) -> TemplateParseResult<TemplateRenderer<'static, Context>> {
3094 parse(
3095 &self.language,
3096 &mut TemplateDiagnostics::new(),
3097 template,
3098 &self.aliases_map,
3099 )
3100 }
3101
3102 fn parse_err(&self, template: &str) -> String {
3103 let err = self
3104 .parse(template)
3105 .err()
3106 .expect("Got unexpected successful template rendering");
3107
3108 iter::successors(Some(&err as &dyn std::error::Error), |e| e.source()).join("\n")
3109 }
3110
3111 fn parse_err_kind(&self, template: &str) -> TemplateParseErrorKind {
3112 self.parse(template)
3113 .err()
3114 .expect("Got unexpected successful template rendering")
3115 .kind()
3116 .clone()
3117 }
3118
3119 fn render_ok(&self, template: &str) -> BString {
3120 let template = self.parse(template).unwrap();
3121 let mut output = Vec::new();
3122 let mut formatter =
3123 ColorFormatter::new(&mut output, self.color_rules.clone().into(), false);
3124 template.format(&Context, &mut formatter).unwrap();
3125 drop(formatter);
3126 output.into()
3127 }
3128
3129 fn render_plain(&self, template: &str) -> BString {
3130 let template = self.parse(template).unwrap();
3131 template.format_plain_text(&Context).into()
3132 }
3133 }
3134
3135 fn literal<'a, O>(value: O) -> TestTemplatePropertyKind
3136 where
3137 O: Clone + 'a,
3138 TestTemplatePropertyKind: WrapTemplateProperty<'a, O>,
3139 {
3140 Literal(value).into_dyn_wrapped()
3141 }
3142
3143 fn new_error_property<'a, O>(message: &'a str) -> TestTemplatePropertyKind
3144 where
3145 TestTemplatePropertyKind: WrapTemplateProperty<'a, O>,
3146 {
3147 Literal(())
3148 .and_then(|()| Err(TemplatePropertyError(message.into())))
3149 .into_dyn_wrapped()
3150 }
3151
3152 fn new_signature(name: &str, email: &str) -> Signature {
3153 Signature {
3154 name: name.to_owned(),
3155 email: email.to_owned(),
3156 timestamp: new_timestamp(0, 0),
3157 }
3158 }
3159
3160 fn new_timestamp(msec: i64, tz_offset: i32) -> Timestamp {
3161 Timestamp {
3162 timestamp: MillisSinceEpoch(msec),
3163 tz_offset,
3164 }
3165 }
3166
3167 #[test]
3168 fn test_parsed_tree() {
3169 let mut env = TestTemplateEnv::new();
3170 env.add_keyword("divergent", || literal(false));
3171 env.add_keyword("empty", || literal(true));
3172 env.add_keyword("hello", || literal("Hello".to_owned()));
3173
3174 insta::assert_snapshot!(env.render_ok(r#" "#), @"");
3176
3177 insta::assert_snapshot!(env.render_ok(r#" hello.upper() "#), @"HELLO");
3179
3180 insta::assert_snapshot!(env.render_ok(r#" hello.upper() ++ true "#), @"HELLOtrue");
3182
3183 insta::assert_snapshot!(env.render_ok(r#"(hello.upper())"#), @"HELLO");
3185
3186 insta::assert_snapshot!(env.render_ok(r#"(hello.upper() ++ " ") ++ empty"#), @"HELLO true");
3188
3189 insta::assert_snapshot!(env.render_ok(r#"if((divergent), "t", "f")"#), @"f");
3191
3192 insta::assert_snapshot!(env.render_ok(r#"(hello).upper()"#), @"HELLO");
3194
3195 insta::assert_snapshot!(env.render_ok("hello\n .upper()"), @"HELLO");
3197 }
3198
3199 #[test]
3200 fn test_parse_error() {
3201 let mut env = TestTemplateEnv::new();
3202 env.add_keyword("description", || literal("".to_owned()));
3203 env.add_keyword("empty", || literal(true));
3204
3205 insta::assert_snapshot!(env.parse_err(r#"foo bar"#), @"
3206 --> 1:5
3207 |
3208 1 | foo bar
3209 | ^---
3210 |
3211 = expected <EOI>, `++`, `||`, `&&`, `==`, `!=`, `>=`, `>`, `<=`, `<`, `+`, `-`, `*`, `/`, or `%`
3212 ");
3213 insta::assert_snapshot!(env.parse_err("1 +"), @"
3214 --> 1:4
3215 |
3216 1 | 1 +
3217 | ^---
3218 |
3219 = expected `!`, `-`, or <primary>
3220 ");
3221 insta::assert_snapshot!(env.parse_err("self.timestamp"), @"
3222 --> 1:6
3223 |
3224 1 | self.timestamp
3225 | ^---
3226 |
3227 = expected <function>
3228 ");
3229
3230 insta::assert_snapshot!(env.parse_err(r#"foo"#), @"
3231 --> 1:1
3232 |
3233 1 | foo
3234 | ^-^
3235 |
3236 = Keyword `foo` doesn't exist
3237 ");
3238
3239 insta::assert_snapshot!(env.parse_err(r#"foo()"#), @"
3240 --> 1:1
3241 |
3242 1 | foo()
3243 | ^-^
3244 |
3245 = Function `foo` doesn't exist
3246 ");
3247 insta::assert_snapshot!(env.parse_err(r#"false()"#), @"
3248 --> 1:1
3249 |
3250 1 | false()
3251 | ^---^
3252 |
3253 = Expected identifier
3254 ");
3255
3256 insta::assert_snapshot!(env.parse_err(r#"!foo"#), @"
3257 --> 1:2
3258 |
3259 1 | !foo
3260 | ^-^
3261 |
3262 = Keyword `foo` doesn't exist
3263 ");
3264 insta::assert_snapshot!(env.parse_err(r#"true && 123"#), @"
3265 --> 1:9
3266 |
3267 1 | true && 123
3268 | ^-^
3269 |
3270 = Expected expression of type `Boolean`, but actual type is `Integer`
3271 ");
3272 insta::assert_snapshot!(env.parse_err(r#"true == 1"#), @"
3273 --> 1:1
3274 |
3275 1 | true == 1
3276 | ^-------^
3277 |
3278 = Cannot compare expressions of type `Boolean` and `Integer`
3279 ");
3280 insta::assert_snapshot!(env.parse_err(r#"true != 'a'"#), @"
3281 --> 1:1
3282 |
3283 1 | true != 'a'
3284 | ^---------^
3285 |
3286 = Cannot compare expressions of type `Boolean` and `String`
3287 ");
3288 insta::assert_snapshot!(env.parse_err(r#"1 == true"#), @"
3289 --> 1:1
3290 |
3291 1 | 1 == true
3292 | ^-------^
3293 |
3294 = Cannot compare expressions of type `Integer` and `Boolean`
3295 ");
3296 insta::assert_snapshot!(env.parse_err(r#"1 != 'a'"#), @"
3297 --> 1:1
3298 |
3299 1 | 1 != 'a'
3300 | ^------^
3301 |
3302 = Cannot compare expressions of type `Integer` and `String`
3303 ");
3304 insta::assert_snapshot!(env.parse_err(r#"'a' == true"#), @"
3305 --> 1:1
3306 |
3307 1 | 'a' == true
3308 | ^---------^
3309 |
3310 = Cannot compare expressions of type `String` and `Boolean`
3311 ");
3312 insta::assert_snapshot!(env.parse_err(r#"'a' != 1"#), @"
3313 --> 1:1
3314 |
3315 1 | 'a' != 1
3316 | ^------^
3317 |
3318 = Cannot compare expressions of type `String` and `Integer`
3319 ");
3320 insta::assert_snapshot!(env.parse_err(r#"'a' == label("", "")"#), @r#"
3321 --> 1:1
3322 |
3323 1 | 'a' == label("", "")
3324 | ^------------------^
3325 |
3326 = Cannot compare expressions of type `String` and `Template`
3327 "#);
3328 insta::assert_snapshot!(env.parse_err(r#"'a' > 1"#), @"
3329 --> 1:1
3330 |
3331 1 | 'a' > 1
3332 | ^-----^
3333 |
3334 = Cannot compare expressions of type `String` and `Integer`
3335 ");
3336
3337 insta::assert_snapshot!(env.parse_err(r#"description.first_line().foo()"#), @"
3338 --> 1:26
3339 |
3340 1 | description.first_line().foo()
3341 | ^-^
3342 |
3343 = Method `foo` doesn't exist for type `String`
3344 ");
3345
3346 insta::assert_snapshot!(env.parse_err(r#"10000000000000000000"#), @"
3347 --> 1:1
3348 |
3349 1 | 10000000000000000000
3350 | ^------------------^
3351 |
3352 = Invalid integer literal
3353 number too large to fit in target type
3354 ");
3355 insta::assert_snapshot!(env.parse_err(r#"42.foo()"#), @"
3356 --> 1:4
3357 |
3358 1 | 42.foo()
3359 | ^-^
3360 |
3361 = Method `foo` doesn't exist for type `Integer`
3362 ");
3363 insta::assert_snapshot!(env.parse_err(r#"(-empty)"#), @"
3364 --> 1:3
3365 |
3366 1 | (-empty)
3367 | ^---^
3368 |
3369 = Expected expression of type `Integer`, but actual type is `Boolean`
3370 ");
3371
3372 insta::assert_snapshot!(env.parse_err(r#"("foo" ++ "bar").baz()"#), @r#"
3373 --> 1:18
3374 |
3375 1 | ("foo" ++ "bar").baz()
3376 | ^-^
3377 |
3378 = Method `baz` doesn't exist for type `Template`
3379 "#);
3380
3381 insta::assert_snapshot!(env.parse_err(r#"description.contains()"#), @"
3382 --> 1:22
3383 |
3384 1 | description.contains()
3385 | ^
3386 |
3387 = Function `contains`: Expected 1 arguments
3388 ");
3389
3390 insta::assert_snapshot!(env.parse_err(r#"description.first_line("foo")"#), @r#"
3391 --> 1:24
3392 |
3393 1 | description.first_line("foo")
3394 | ^---^
3395 |
3396 = Function `first_line`: Expected 0 arguments
3397 "#);
3398
3399 insta::assert_snapshot!(env.parse_err(r#"label()"#), @"
3400 --> 1:7
3401 |
3402 1 | label()
3403 | ^
3404 |
3405 = Function `label`: Expected 2 arguments
3406 ");
3407 insta::assert_snapshot!(env.parse_err(r#"label("foo", "bar", "baz")"#), @r#"
3408 --> 1:7
3409 |
3410 1 | label("foo", "bar", "baz")
3411 | ^-----------------^
3412 |
3413 = Function `label`: Expected 2 arguments
3414 "#);
3415
3416 insta::assert_snapshot!(env.parse_err(r#"if()"#), @"
3417 --> 1:4
3418 |
3419 1 | if()
3420 | ^
3421 |
3422 = Function `if`: Expected 2 to 3 arguments
3423 ");
3424 insta::assert_snapshot!(env.parse_err(r#"if("foo", "bar", "baz", "quux")"#), @r#"
3425 --> 1:4
3426 |
3427 1 | if("foo", "bar", "baz", "quux")
3428 | ^-------------------------^
3429 |
3430 = Function `if`: Expected 2 to 3 arguments
3431 "#);
3432
3433 insta::assert_snapshot!(env.parse_err(r#"pad_start("foo", fill_char = "bar", "baz")"#), @r#"
3434 --> 1:37
3435 |
3436 1 | pad_start("foo", fill_char = "bar", "baz")
3437 | ^---^
3438 |
3439 = Function `pad_start`: Positional argument follows keyword argument
3440 "#);
3441
3442 insta::assert_snapshot!(env.parse_err(r#"if(label("foo", "bar"), "baz")"#), @r#"
3443 --> 1:4
3444 |
3445 1 | if(label("foo", "bar"), "baz")
3446 | ^-----------------^
3447 |
3448 = Expected expression of type `Boolean`, but actual type is `Template`
3449 "#);
3450
3451 insta::assert_snapshot!(env.parse_err(r#"|x| description"#), @"
3452 --> 1:1
3453 |
3454 1 | |x| description
3455 | ^-------------^
3456 |
3457 = Lambda cannot be defined here
3458 ");
3459 }
3460
3461 #[test]
3462 fn test_self_keyword() {
3463 let mut env = TestTemplateEnv::new();
3464 env.add_keyword("say_hello", || literal("Hello".to_owned()));
3465
3466 insta::assert_snapshot!(env.render_ok(r#"self.say_hello()"#), @"Hello");
3467 insta::assert_snapshot!(env.parse_err(r#"self"#), @"
3468 --> 1:1
3469 |
3470 1 | self
3471 | ^--^
3472 |
3473 = Expected expression of type `Template`, but actual type is `Self`
3474 ");
3475 }
3476
3477 #[test]
3478 fn test_boolean_cast() {
3479 let mut env = TestTemplateEnv::new();
3480
3481 env.add_keyword("empty_bstr", || literal(BString::from("")));
3482 env.add_keyword("nonempty_bstr", || literal(BString::from("a")));
3483 insta::assert_snapshot!(env.render_ok("if(empty_bstr, true, false)"), @"false");
3484 insta::assert_snapshot!(env.render_ok("if(nonempty_bstr, true, false)"), @"true");
3485
3486 env.add_keyword("empty_bstr_list", || literal::<Vec<BString>>(vec![]));
3487 env.add_keyword("nonempty_bstr_list", || literal(vec![BString::from("")]));
3488 insta::assert_snapshot!(env.render_ok("if(empty_bstr_list, true, false)"), @"false");
3489 insta::assert_snapshot!(env.render_ok("if(nonempty_bstr_list, true, false)"), @"true");
3490
3491 insta::assert_snapshot!(env.render_ok(r#"if("", true, false)"#), @"false");
3492 insta::assert_snapshot!(env.render_ok(r#"if("a", true, false)"#), @"true");
3493
3494 env.add_keyword("sl0", || literal::<Vec<String>>(vec![]));
3495 env.add_keyword("sl1", || literal(vec!["".to_owned()]));
3496 insta::assert_snapshot!(env.render_ok(r#"if(sl0, true, false)"#), @"false");
3497 insta::assert_snapshot!(env.render_ok(r#"if(sl1, true, false)"#), @"true");
3498
3499 insta::assert_snapshot!(env.parse_err(r#"if(0, true, false)"#), @"
3501 --> 1:4
3502 |
3503 1 | if(0, true, false)
3504 | ^
3505 |
3506 = Expected expression of type `Boolean`, but actual type is `Integer`
3507 ");
3508
3509 env.add_keyword("none_i64", || literal(None::<i64>));
3511 env.add_keyword("some_i64", || literal(Some(0)));
3512 insta::assert_snapshot!(env.render_ok(r#"if(none_i64, true, false)"#), @"false");
3513 insta::assert_snapshot!(env.render_ok(r#"if(some_i64, true, false)"#), @"true");
3514
3515 insta::assert_snapshot!(
3517 env.render_ok("if(-none_i64 == 1, true, false)"),
3518 @"<Error: No Integer available>"
3519 );
3520
3521 insta::assert_snapshot!(env.parse_err(r#"if(label("", ""), true, false)"#), @r#"
3522 --> 1:4
3523 |
3524 1 | if(label("", ""), true, false)
3525 | ^-----------^
3526 |
3527 = Expected expression of type `Boolean`, but actual type is `Template`
3528 "#);
3529 insta::assert_snapshot!(env.parse_err(r#"if(sl0.map(|x| x), true, false)"#), @"
3530 --> 1:4
3531 |
3532 1 | if(sl0.map(|x| x), true, false)
3533 | ^------------^
3534 |
3535 = Expected expression of type `Boolean`, but actual type is `AnyList`
3536 ");
3537
3538 env.add_keyword("empty_email", || literal(Email("".to_owned())));
3539 env.add_keyword("nonempty_email", || {
3540 literal(Email("local@domain".to_owned()))
3541 });
3542 insta::assert_snapshot!(env.render_ok(r#"if(empty_email, true, false)"#), @"false");
3543 insta::assert_snapshot!(env.render_ok(r#"if(nonempty_email, true, false)"#), @"true");
3544
3545 env.add_keyword("config_bool", || literal(ConfigValue::from(true)));
3547 insta::assert_snapshot!(env.parse_err("if(config_bool, true, false)"), @"
3548 --> 1:4
3549 |
3550 1 | if(config_bool, true, false)
3551 | ^---------^
3552 |
3553 = Expected expression of type `Boolean`, but actual type is `ConfigValue`
3554 ");
3555
3556 env.add_keyword("signature", || {
3558 literal(new_signature("Test User", "test.user@example.com"))
3559 });
3560 env.add_keyword("size_hint", || literal((5, None)));
3561 env.add_keyword("timestamp", || literal(new_timestamp(0, 0)));
3562 env.add_keyword("timestamp_range", || {
3563 literal(TimestampRange {
3564 start: new_timestamp(0, 0),
3565 end: new_timestamp(0, 0),
3566 })
3567 });
3568 assert_matches!(
3569 env.parse_err_kind("if(signature, true, false)"),
3570 TemplateParseErrorKind::Expression(_)
3571 );
3572 assert_matches!(
3573 env.parse_err_kind("if(size_hint, true, false)"),
3574 TemplateParseErrorKind::Expression(_)
3575 );
3576 assert_matches!(
3577 env.parse_err_kind("if(timestamp, true, false)"),
3578 TemplateParseErrorKind::Expression(_)
3579 );
3580 assert_matches!(
3581 env.parse_err_kind("if(timestamp_range, true, false)"),
3582 TemplateParseErrorKind::Expression(_)
3583 );
3584 assert_matches!(
3585 env.parse_err_kind("if(if(true, true), true, false)"),
3586 TemplateParseErrorKind::Expression(_)
3587 );
3588 assert_matches!(
3589 env.parse_err_kind("if(sl0.map(|s| s), true, false)"),
3590 TemplateParseErrorKind::Expression(_)
3591 );
3592 }
3593
3594 #[test]
3595 fn test_arithmetic_operation() {
3596 let mut env = TestTemplateEnv::new();
3597 env.add_keyword("none_i64", || literal(None::<i64>));
3598 env.add_keyword("some_i64", || literal(Some(1)));
3599 env.add_keyword("i64_min", || literal(i64::MIN));
3600 env.add_keyword("i64_max", || literal(i64::MAX));
3601
3602 insta::assert_snapshot!(env.render_ok(r#"-1"#), @"-1");
3603 insta::assert_snapshot!(env.render_ok(r#"--2"#), @"2");
3604 insta::assert_snapshot!(env.render_ok(r#"-(3)"#), @"-3");
3605 insta::assert_snapshot!(env.render_ok(r#"1 + 2"#), @"3");
3606 insta::assert_snapshot!(env.render_ok(r#"2 * 3"#), @"6");
3607 insta::assert_snapshot!(env.render_ok(r#"1 + 2 * 3"#), @"7");
3608 insta::assert_snapshot!(env.render_ok(r#"4 / 2"#), @"2");
3609 insta::assert_snapshot!(env.render_ok(r#"5 / 2"#), @"2");
3610 insta::assert_snapshot!(env.render_ok(r#"5 % 2"#), @"1");
3611
3612 insta::assert_snapshot!(env.render_ok(r#"-none_i64"#), @"<Error: No Integer available>");
3615 insta::assert_snapshot!(env.render_ok(r#"-some_i64"#), @"-1");
3616 insta::assert_snapshot!(env.render_ok(r#"some_i64 + some_i64"#), @"2");
3617 insta::assert_snapshot!(env.render_ok(r#"some_i64 + none_i64"#), @"<Error: No Integer available>");
3618 insta::assert_snapshot!(env.render_ok(r#"none_i64 + some_i64"#), @"<Error: No Integer available>");
3619 insta::assert_snapshot!(env.render_ok(r#"none_i64 + none_i64"#), @"<Error: No Integer available>");
3620
3621 insta::assert_snapshot!(
3623 env.render_ok(r#"-i64_min"#),
3624 @"<Error: Attempt to negate with overflow>");
3625 insta::assert_snapshot!(
3626 env.render_ok(r#"i64_max + 1"#),
3627 @"<Error: Attempt to add with overflow>");
3628 insta::assert_snapshot!(
3629 env.render_ok(r#"i64_min - 1"#),
3630 @"<Error: Attempt to subtract with overflow>");
3631 insta::assert_snapshot!(
3632 env.render_ok(r#"i64_max * 2"#),
3633 @"<Error: Attempt to multiply with overflow>");
3634 insta::assert_snapshot!(
3635 env.render_ok(r#"i64_min / -1"#),
3636 @"<Error: Attempt to divide with overflow>");
3637 insta::assert_snapshot!(
3638 env.render_ok(r#"1 / 0"#),
3639 @"<Error: Attempt to divide by zero>");
3640 insta::assert_snapshot!(
3641 env.render_ok("i64_min % -1"),
3642 @"<Error: Attempt to divide with overflow>");
3643 insta::assert_snapshot!(
3644 env.render_ok(r#"1 % 0"#),
3645 @"<Error: Attempt to divide by zero>");
3646 }
3647
3648 #[test]
3649 fn test_relational_operation() {
3650 let mut env = TestTemplateEnv::new();
3651 env.add_keyword("none_i64", || literal(None::<i64>));
3652 env.add_keyword("some_i64_0", || literal(Some(0_i64)));
3653 env.add_keyword("some_i64_1", || literal(Some(1_i64)));
3654
3655 insta::assert_snapshot!(env.render_ok(r#"1 >= 1"#), @"true");
3656 insta::assert_snapshot!(env.render_ok(r#"0 >= 1"#), @"false");
3657 insta::assert_snapshot!(env.render_ok(r#"2 > 1"#), @"true");
3658 insta::assert_snapshot!(env.render_ok(r#"1 > 1"#), @"false");
3659 insta::assert_snapshot!(env.render_ok(r#"1 <= 1"#), @"true");
3660 insta::assert_snapshot!(env.render_ok(r#"2 <= 1"#), @"false");
3661 insta::assert_snapshot!(env.render_ok(r#"0 < 1"#), @"true");
3662 insta::assert_snapshot!(env.render_ok(r#"1 < 1"#), @"false");
3663
3664 insta::assert_snapshot!(env.render_ok(r#"none_i64 < some_i64_0"#), @"true");
3666 insta::assert_snapshot!(env.render_ok(r#"some_i64_0 > some_i64_1"#), @"false");
3667 insta::assert_snapshot!(env.render_ok(r#"none_i64 < 0"#), @"true");
3668 insta::assert_snapshot!(env.render_ok(r#"1 > some_i64_0"#), @"true");
3669
3670 assert_matches!(
3672 env.parse_err_kind("42 >= true"),
3673 TemplateParseErrorKind::Expression(_)
3674 );
3675 assert_matches!(
3676 env.parse_err_kind("none_i64 >= true"),
3677 TemplateParseErrorKind::Expression(_)
3678 );
3679
3680 env.add_keyword("str_list", || {
3682 literal(vec!["foo".to_owned(), "bar".to_owned()])
3683 });
3684 env.add_keyword("cfg_val", || {
3685 literal(ConfigValue::from_iter([("foo", "bar")]))
3686 });
3687 env.add_keyword("some_cfg", || literal(Some(ConfigValue::from(1))));
3688 env.add_keyword("signature", || {
3689 literal(new_signature("User", "user@example.com"))
3690 });
3691 env.add_keyword("email", || literal(Email("me@example.com".to_owned())));
3692 env.add_keyword("size_hint", || literal((10, None)));
3693 env.add_keyword("timestamp", || literal(new_timestamp(0, 0)));
3694 env.add_keyword("timestamp_range", || {
3695 literal(TimestampRange {
3696 start: new_timestamp(0, 0),
3697 end: new_timestamp(0, 0),
3698 })
3699 });
3700 assert_matches!(
3701 env.parse_err_kind("'a' >= 'a'"),
3702 TemplateParseErrorKind::Expression(_)
3703 );
3704 assert_matches!(
3705 env.parse_err_kind("str_list >= str_list"),
3706 TemplateParseErrorKind::Expression(_)
3707 );
3708 assert_matches!(
3709 env.parse_err_kind("true >= true"),
3710 TemplateParseErrorKind::Expression(_)
3711 );
3712 assert_matches!(
3713 env.parse_err_kind("cfg_val >= cfg_val"),
3714 TemplateParseErrorKind::Expression(_)
3715 );
3716 assert_matches!(
3717 env.parse_err_kind("some_cfg >= some_cfg"),
3718 TemplateParseErrorKind::Expression(_)
3719 );
3720 assert_matches!(
3721 env.parse_err_kind("signature >= signature"),
3722 TemplateParseErrorKind::Expression(_)
3723 );
3724 assert_matches!(
3725 env.parse_err_kind("email >= email"),
3726 TemplateParseErrorKind::Expression(_)
3727 );
3728 assert_matches!(
3729 env.parse_err_kind("size_hint >= size_hint"),
3730 TemplateParseErrorKind::Expression(_)
3731 );
3732 assert_matches!(
3733 env.parse_err_kind("timestamp >= timestamp"),
3734 TemplateParseErrorKind::Expression(_)
3735 );
3736 assert_matches!(
3737 env.parse_err_kind("timestamp_range >= timestamp_range"),
3738 TemplateParseErrorKind::Expression(_)
3739 );
3740 assert_matches!(
3741 env.parse_err_kind("label('', '') >= label('', '')"),
3742 TemplateParseErrorKind::Expression(_)
3743 );
3744 assert_matches!(
3745 env.parse_err_kind("if(true, true) >= if(true, true)"),
3746 TemplateParseErrorKind::Expression(_)
3747 );
3748 assert_matches!(
3749 env.parse_err_kind("str_list.map(|s| s) >= str_list.map(|s| s)"),
3750 TemplateParseErrorKind::Expression(_)
3751 );
3752 }
3753
3754 #[test]
3755 fn test_logical_operation() {
3756 let mut env = TestTemplateEnv::new();
3757 env.add_keyword("none_i64", || literal::<Option<i64>>(None));
3758 env.add_keyword("some_i64_0", || literal(Some(0_i64)));
3759 env.add_keyword("some_i64_1", || literal(Some(1_i64)));
3760 env.add_keyword("bstr1", || literal(BString::from("1")));
3761 env.add_keyword("bstr2", || literal(BString::from("2")));
3762 env.add_keyword("email1", || literal(Email("local-1@domain".to_owned())));
3763 env.add_keyword("email2", || literal(Email("local-2@domain".to_owned())));
3764
3765 insta::assert_snapshot!(env.render_ok(r#"!false"#), @"true");
3766 insta::assert_snapshot!(env.render_ok(r#"false || !false"#), @"true");
3767 insta::assert_snapshot!(env.render_ok(r#"false && true"#), @"false");
3768 insta::assert_snapshot!(env.render_ok(r#"true == true"#), @"true");
3769 insta::assert_snapshot!(env.render_ok(r#"true == false"#), @"false");
3770 insta::assert_snapshot!(env.render_ok(r#"true != true"#), @"false");
3771 insta::assert_snapshot!(env.render_ok(r#"true != false"#), @"true");
3772
3773 insta::assert_snapshot!(env.render_ok(r#"1 == 1"#), @"true");
3774 insta::assert_snapshot!(env.render_ok(r#"1 == 2"#), @"false");
3775 insta::assert_snapshot!(env.render_ok(r#"1 != 1"#), @"false");
3776 insta::assert_snapshot!(env.render_ok(r#"1 != 2"#), @"true");
3777 insta::assert_snapshot!(env.render_ok(r#"none_i64 == none_i64"#), @"true");
3778 insta::assert_snapshot!(env.render_ok(r#"some_i64_0 != some_i64_0"#), @"false");
3779 insta::assert_snapshot!(env.render_ok(r#"none_i64 == 0"#), @"false");
3780 insta::assert_snapshot!(env.render_ok(r#"some_i64_0 != 0"#), @"false");
3781 insta::assert_snapshot!(env.render_ok(r#"1 == some_i64_1"#), @"true");
3782
3783 insta::assert_snapshot!(env.render_ok("bstr1 == bstr1"), @"true");
3784 insta::assert_snapshot!(env.render_ok("bstr1 == bstr2"), @"false");
3785 insta::assert_snapshot!(env.render_ok("bstr1 == '1'"), @"true");
3786 insta::assert_snapshot!(env.render_ok("'2' != bstr2"), @"false");
3787
3788 insta::assert_snapshot!(env.render_ok(r#"'a' == 'a'"#), @"true");
3789 insta::assert_snapshot!(env.render_ok(r#"'a' == 'b'"#), @"false");
3790 insta::assert_snapshot!(env.render_ok(r#"'a' != 'a'"#), @"false");
3791 insta::assert_snapshot!(env.render_ok(r#"'a' != 'b'"#), @"true");
3792 insta::assert_snapshot!(env.render_ok(r#"email1 == email1"#), @"true");
3793 insta::assert_snapshot!(env.render_ok(r#"email1 == email2"#), @"false");
3794 insta::assert_snapshot!(env.render_ok(r#"email1 == 'local-1@domain'"#), @"true");
3795 insta::assert_snapshot!(env.render_ok(r#"email1 != 'local-2@domain'"#), @"true");
3796 insta::assert_snapshot!(env.render_ok(r#"'local-1@domain' == email1"#), @"true");
3797 insta::assert_snapshot!(env.render_ok(r#"'local-2@domain' != email1"#), @"true");
3798
3799 insta::assert_snapshot!(env.render_ok(r#" !"" "#), @"true");
3800 insta::assert_snapshot!(env.render_ok(r#" "" || "a".lines() "#), @"true");
3801
3802 env.add_keyword("bad_bool", || new_error_property::<bool>("Bad"));
3804 insta::assert_snapshot!(env.render_ok(r#"false && bad_bool"#), @"false");
3805 insta::assert_snapshot!(env.render_ok(r#"true && bad_bool"#), @"<Error: Bad>");
3806 insta::assert_snapshot!(env.render_ok(r#"false || bad_bool"#), @"<Error: Bad>");
3807 insta::assert_snapshot!(env.render_ok(r#"true || bad_bool"#), @"true");
3808
3809 assert_matches!(
3811 env.parse_err_kind("some_i64_0 == '0'"),
3812 TemplateParseErrorKind::Expression(_)
3813 );
3814 assert_matches!(
3815 env.parse_err_kind("email1 == 42"),
3816 TemplateParseErrorKind::Expression(_)
3817 );
3818
3819 env.add_keyword("str_list", || {
3821 literal(vec!["foo".to_owned(), "bar".to_owned()])
3822 });
3823 env.add_keyword("cfg_val", || {
3824 literal(ConfigValue::from_iter([("foo", "bar")]))
3825 });
3826 env.add_keyword("some_cfg", || literal(Some(ConfigValue::from(true))));
3827 env.add_keyword("signature", || {
3828 literal(new_signature("User", "user@example.com"))
3829 });
3830 env.add_keyword("size_hint", || literal((10, None)));
3831 env.add_keyword("timestamp", || literal(new_timestamp(0, 0)));
3832 env.add_keyword("timestamp_range", || {
3833 literal(TimestampRange {
3834 start: new_timestamp(0, 0),
3835 end: new_timestamp(0, 0),
3836 })
3837 });
3838 assert_matches!(
3839 env.parse_err_kind("str_list == str_list"),
3840 TemplateParseErrorKind::Expression(_)
3841 );
3842 assert_matches!(
3843 env.parse_err_kind("cfg_val == cfg_val"),
3844 TemplateParseErrorKind::Expression(_)
3845 );
3846 assert_matches!(
3847 env.parse_err_kind("some_cfg == some_cfg"),
3848 TemplateParseErrorKind::Expression(_)
3849 );
3850 assert_matches!(
3851 env.parse_err_kind("signature == signature"),
3852 TemplateParseErrorKind::Expression(_)
3853 );
3854 assert_matches!(
3855 env.parse_err_kind("size_hint == size_hint"),
3856 TemplateParseErrorKind::Expression(_)
3857 );
3858 assert_matches!(
3859 env.parse_err_kind("timestamp == timestamp"),
3860 TemplateParseErrorKind::Expression(_)
3861 );
3862 assert_matches!(
3863 env.parse_err_kind("timestamp_range == timestamp_range"),
3864 TemplateParseErrorKind::Expression(_)
3865 );
3866 assert_matches!(
3867 env.parse_err_kind("label('', '') == label('', '')"),
3868 TemplateParseErrorKind::Expression(_)
3869 );
3870 assert_matches!(
3871 env.parse_err_kind("if(true, true) == if(true, true)"),
3872 TemplateParseErrorKind::Expression(_)
3873 );
3874 assert_matches!(
3875 env.parse_err_kind("str_list.map(|s| s) == str_list.map(|s| s)"),
3876 TemplateParseErrorKind::Expression(_)
3877 );
3878 }
3879
3880 #[test]
3881 fn test_list_method() {
3882 let mut env = TestTemplateEnv::new();
3883 env.add_keyword("empty", || literal(true));
3884 env.add_keyword("sep", || literal("sep".to_owned()));
3885
3886 insta::assert_snapshot!(env.render_ok(r#""".lines().len()"#), @"0");
3887 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().len()"#), @"3");
3888
3889 insta::assert_snapshot!(env.render_ok(r#""".lines().join("|")"#), @"");
3890 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().join("|")"#), @"a|b|c");
3891 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().join("\0")"#), @"a\0b\0c");
3893 insta::assert_snapshot!(
3895 env.render_ok(r#""a\nb\nc".lines().join(sep.upper())"#),
3896 @"aSEPbSEPc");
3897
3898 insta::assert_snapshot!(
3899 env.render_ok(r#""a\nbb\nc".lines().filter(|s| s.len() == 1)"#),
3900 @"a c");
3901
3902 insta::assert_snapshot!(
3903 env.render_ok(r#""a\nb\nc".lines().map(|s| s ++ s)"#),
3904 @"aa bb cc");
3905
3906 insta::assert_snapshot!(
3908 env.render_ok(r#""a\nb\nc".lines().any(|s| s == "b")"#),
3909 @"true");
3910 insta::assert_snapshot!(
3911 env.render_ok(r#""a\nb\nc".lines().any(|s| s == "d")"#),
3912 @"false");
3913 insta::assert_snapshot!(
3914 env.render_ok(r#""".lines().any(|s| s == "a")"#),
3915 @"false");
3916 insta::assert_snapshot!(
3918 env.render_ok(r#""ax\nbb\nc".lines().any(|s| s.contains("x"))"#),
3919 @"true");
3920 insta::assert_snapshot!(
3921 env.render_ok(r#""a\nbb\nc".lines().any(|s| s.len() > 1)"#),
3922 @"true");
3923
3924 insta::assert_snapshot!(
3926 env.render_ok(r#""a\nb\nc".lines().all(|s| s.len() == 1)"#),
3927 @"true");
3928 insta::assert_snapshot!(
3929 env.render_ok(r#""a\nbb\nc".lines().all(|s| s.len() == 1)"#),
3930 @"false");
3931 insta::assert_snapshot!(
3933 env.render_ok(r#""".lines().all(|s| s == "a")"#),
3934 @"true");
3935 insta::assert_snapshot!(
3937 env.render_ok(r#""ax\nbx\ncx".lines().all(|s| s.ends_with("x"))"#),
3938 @"true");
3939 insta::assert_snapshot!(
3940 env.render_ok(r#""a\nbb\nc".lines().all(|s| s.len() < 3)"#),
3941 @"true");
3942
3943 insta::assert_snapshot!(
3945 env.render_ok(r#""a\nbb\nccc".lines().filter(|s| s.len() > 1).any(|s| s == "bb")"#),
3946 @"true");
3947 insta::assert_snapshot!(
3948 env.render_ok(r#""a\nbb\nccc".lines().filter(|s| s.len() > 1).all(|s| s.len() >= 2)"#),
3949 @"true");
3950
3951 insta::assert_snapshot!(
3953 env.render_ok(r#"if("a\nb".lines().any(|s| s == "a"), "found", "not found")"#),
3954 @"found");
3955 insta::assert_snapshot!(
3956 env.render_ok(r#"if("a\nb".lines().all(|s| s.len() == 1), "all single", "not all")"#),
3957 @"all single");
3958
3959 insta::assert_snapshot!(
3961 env.render_ok(r#""a\nb\nc".lines().map(|s| s ++ empty)"#),
3962 @"atrue btrue ctrue");
3963 insta::assert_snapshot!(
3965 env.render_ok(r#""a\nb\nc".lines().map(|self| self ++ empty)"#),
3966 @"atrue btrue ctrue");
3967 insta::assert_snapshot!(
3969 env.render_ok(r#""a\nb\nc".lines().map(|empty| empty)"#),
3970 @"a b c");
3971 insta::assert_snapshot!(
3973 env.render_ok(r#""a\nb\nc".lines().map(|s| "x\ny".lines().map(|t| s ++ t))"#),
3974 @"ax ay bx by cx cy");
3975 insta::assert_snapshot!(
3977 env.render_ok(r#""a\nb\nc".lines().map(|s| "x\ny".lines().map(|t| s ++ t).join(",")).join(";")"#),
3978 @"ax,ay;bx,by;cx,cy");
3979 insta::assert_snapshot!(
3981 env.render_ok(r#""! a\n!b\nc\n end".remove_suffix("end").trim_end().lines().map(|s| s.remove_prefix("!").trim_start())"#),
3982 @"a b c");
3983
3984 env.add_alias("identity", "|x| x");
3986 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().map(identity)"#), @"a b c");
3987
3988 insta::assert_snapshot!(env.parse_err(r#""a".lines().map(empty)"#), @r#"
3990 --> 1:17
3991 |
3992 1 | "a".lines().map(empty)
3993 | ^---^
3994 |
3995 = Expected lambda expression
3996 "#);
3997 insta::assert_snapshot!(env.parse_err(r#""a".lines().map(|| "")"#), @r#"
3999 --> 1:18
4000 |
4001 1 | "a".lines().map(|| "")
4002 | ^
4003 |
4004 = Expected 1 lambda parameters
4005 "#);
4006 insta::assert_snapshot!(env.parse_err(r#""a".lines().map(|a, b| "")"#), @r#"
4007 --> 1:18
4008 |
4009 1 | "a".lines().map(|a, b| "")
4010 | ^--^
4011 |
4012 = Expected 1 lambda parameters
4013 "#);
4014 insta::assert_snapshot!(env.parse_err(r#""a".lines().filter(|s| s ++ "\n")"#), @r#"
4016 --> 1:24
4017 |
4018 1 | "a".lines().filter(|s| s ++ "\n")
4019 | ^-------^
4020 |
4021 = Expected expression of type `Boolean`, but actual type is `Template`
4022 "#);
4023
4024 insta::assert_snapshot!(env.parse_err(r#""a".lines().any(|s| s.len())"#), @r#"
4026 --> 1:21
4027 |
4028 1 | "a".lines().any(|s| s.len())
4029 | ^-----^
4030 |
4031 = Expected expression of type `Boolean`, but actual type is `Integer`
4032 "#);
4033 insta::assert_snapshot!(env.parse_err(r#""a".lines().all(|s| s ++ "x")"#), @r#"
4035 --> 1:21
4036 |
4037 1 | "a".lines().all(|s| s ++ "x")
4038 | ^------^
4039 |
4040 = Expected expression of type `Boolean`, but actual type is `Template`
4041 "#);
4042 insta::assert_snapshot!(env.parse_err(r#""a".lines().any(|| true)"#), @r#"
4044 --> 1:18
4045 |
4046 1 | "a".lines().any(|| true)
4047 | ^
4048 |
4049 = Expected 1 lambda parameters
4050 "#);
4051 insta::assert_snapshot!(env.parse_err(r#""a".lines().all(|a, b| true)"#), @r#"
4053 --> 1:18
4054 |
4055 1 | "a".lines().all(|a, b| true)
4056 | ^--^
4057 |
4058 = Expected 1 lambda parameters
4059 "#);
4060 insta::assert_snapshot!(env.parse_err(r#""a".lines().map(|s| s.unknown())"#), @r#"
4062 --> 1:23
4063 |
4064 1 | "a".lines().map(|s| s.unknown())
4065 | ^-----^
4066 |
4067 = Method `unknown` doesn't exist for type `String`
4068 "#);
4069 env.add_alias("too_many_params", "|x, y| x");
4071 insta::assert_snapshot!(env.parse_err(r#""a".lines().map(too_many_params)"#), @r#"
4072 --> 1:17
4073 |
4074 1 | "a".lines().map(too_many_params)
4075 | ^-------------^
4076 |
4077 = In alias `too_many_params`
4078 --> 1:2
4079 |
4080 1 | |x, y| x
4081 | ^--^
4082 |
4083 = Expected 1 lambda parameters
4084 "#);
4085
4086 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().first()"#), @"a");
4088 insta::assert_snapshot!(env.render_ok(r#""".lines().first()"#), @"<Error: List is empty>");
4089
4090 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().last()"#), @"c");
4092 insta::assert_snapshot!(env.render_ok(r#""".lines().last()"#), @"<Error: List is empty>");
4093
4094 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().get(0)"#), @"a");
4096 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().get(1)"#), @"b");
4097 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().get(2)"#), @"c");
4098 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().get(3)"#), @"<Error: Index 3 out of bounds>");
4099 insta::assert_snapshot!(env.render_ok(r#""".lines().get(0)"#), @"<Error: Index 0 out of bounds>");
4100
4101 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().reverse().join("|")"#), @"c|b|a");
4103 insta::assert_snapshot!(env.render_ok(r#""".lines().reverse().join("|")"#), @"");
4104
4105 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().skip(0).join("|")"#), @"a|b|c");
4107 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().skip(1).join("|")"#), @"b|c");
4108 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().skip(2).join("|")"#), @"c");
4109 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().skip(3).join("|")"#), @"");
4110 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().skip(10).join("|")"#), @"");
4111
4112 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().take(0).join("|")"#), @"");
4114 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().take(1).join("|")"#), @"a");
4115 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().take(2).join("|")"#), @"a|b");
4116 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().take(3).join("|")"#), @"a|b|c");
4117 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().take(10).join("|")"#), @"a|b|c");
4118
4119 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc\nd".lines().skip(1).take(2).join("|")"#), @"b|c");
4121 }
4122
4123 #[test]
4124 fn test_byte_string_method() {
4125 let mut env = TestTemplateEnv::new();
4126 env.add_keyword("empty", || literal(BString::from("")));
4127 env.add_keyword("foo", || literal(BString::from("foo")));
4128 env.add_keyword("bar", || literal(BString::from("bar")));
4129 env.add_keyword("foobar", || literal(BString::from("foobar")));
4130 env.add_keyword("foo_ws", || literal(BString::from(" \n \r foo \t \r ")));
4131 env.add_keyword("foo_bar_nl", || literal(BString::from("foo\nbar\n")));
4132 env.add_keyword("foo_bar_case", || literal(BString::from("foo BAR")));
4133 env.add_keyword("odd", || literal(BString::from(b"\x80")));
4134 env.add_keyword("odd_ws", || literal(BString::from(b" \x80 ")));
4135 env.add_keyword("odd_case", || literal(BString::from(b"A\x80z")));
4136
4137 insta::assert_snapshot!(env.render_ok("empty.len()"), @"0");
4138 insta::assert_snapshot!(env.render_ok("foo.len()"), @"3");
4139 insta::assert_snapshot!(env.render_ok("odd.len()"), @"1");
4140
4141 insta::assert_snapshot!(env.render_ok("foobar.contains(foo)"), @"true");
4142 insta::assert_snapshot!(env.render_ok("foo.contains(foobar)"), @"false");
4143 insta::assert_snapshot!(env.render_ok("foo.contains('foo')"), @"true");
4144 insta::assert_snapshot!(env.render_ok("odd_case.contains(odd)"), @"true");
4145
4146 insta::assert_snapshot!(env.render_ok("foobar.match(regex:'[a-f]o+')"), @"foo");
4147 insta::assert_snapshot!(env.render_ok("foobar.match(regex:'^$')"), @"");
4148 insta::assert_snapshot!(env.render_ok("json(odd.match(regex:'(?-u:.)'))"), @"[128]");
4149
4150 insta::assert_snapshot!(env.render_ok("foobar.starts_with(foo)"), @"true");
4151 insta::assert_snapshot!(env.render_ok("foobar.starts_with(bar)"), @"false");
4152 insta::assert_snapshot!(env.render_ok("foobar.starts_with('foo')"), @"true");
4153 insta::assert_snapshot!(env.render_ok("foobar.ends_with(foo)"), @"false");
4154 insta::assert_snapshot!(env.render_ok("foobar.ends_with(bar)"), @"true");
4155 insta::assert_snapshot!(env.render_ok("foobar.ends_with('foo')"), @"false");
4156 insta::assert_snapshot!(env.render_ok("odd_case.starts_with('A' ++ odd)"), @"true");
4157 insta::assert_snapshot!(env.render_ok("odd_case.ends_with(odd ++ 'z')"), @"true");
4158
4159 insta::assert_snapshot!(env.render_ok("foobar.remove_prefix(foo)"), @"bar");
4160 insta::assert_snapshot!(env.render_ok("foobar.remove_prefix(bar)"), @"foobar");
4161 insta::assert_snapshot!(env.render_ok("foobar.remove_prefix('foo')"), @"bar");
4162 insta::assert_snapshot!(env.render_ok("foobar.remove_suffix(foo)"), @"foobar");
4163 insta::assert_snapshot!(env.render_ok("foobar.remove_suffix(bar)"), @"foo");
4164 insta::assert_snapshot!(env.render_ok("foobar.remove_suffix('foo')"), @"foobar");
4165 insta::assert_snapshot!(env.render_ok("json(odd_case.remove_prefix('A'))"), @"[128,122]");
4166 insta::assert_snapshot!(env.render_ok("json(odd_case.remove_prefix('A' ++ odd))"), @"[122]");
4167 insta::assert_snapshot!(env.render_ok("json(odd_case.remove_suffix('z'))"), @"[65,128]");
4168 insta::assert_snapshot!(env.render_ok("json(odd_case.remove_suffix(odd ++ 'z'))"), @"[65]");
4169
4170 insta::assert_snapshot!(env.render_ok("'|' ++ foo_ws.trim() ++ '|'"), @"|foo|");
4171 insta::assert_snapshot!(env.render_ok("'|' ++ foo_ws.trim_start() ++ '|'"), @"|foo \t \r |");
4172 insta::assert_snapshot!(env.render_ok("'|' ++ foo_ws.trim_end() ++ '|'"), @"| \n \r foo|");
4173 insta::assert_snapshot!(env.render_ok("json(odd_ws.trim())"), @"[128]");
4174 insta::assert_snapshot!(env.render_ok("json(odd_ws.trim_start())"), @"[128,32]");
4175 insta::assert_snapshot!(env.render_ok("json(odd_ws.trim_end())"), @"[32,128]");
4176
4177 insta::assert_snapshot!(env.render_ok("bar.substr(0)"), @"bar");
4178 insta::assert_snapshot!(env.render_ok("bar.substr(1)"), @"ar");
4179 insta::assert_snapshot!(env.render_ok("bar.substr(2)"), @"r");
4180 insta::assert_snapshot!(env.render_ok("bar.substr(3)"), @"");
4181 insta::assert_snapshot!(env.render_ok("bar.substr(4)"), @"");
4182 insta::assert_snapshot!(env.render_ok("bar.substr(-1)"), @"r");
4183 insta::assert_snapshot!(env.render_ok("bar.substr(-2)"), @"ar");
4184 insta::assert_snapshot!(env.render_ok("bar.substr(-3)"), @"bar");
4185 insta::assert_snapshot!(env.render_ok("bar.substr(-4)"), @"bar");
4186 insta::assert_snapshot!(env.render_ok("bar.substr(1, 0)"), @"");
4187 insta::assert_snapshot!(env.render_ok("bar.substr(1, 2)"), @"a");
4188 insta::assert_snapshot!(env.render_ok("bar.substr(1, -1)"), @"a");
4189 insta::assert_snapshot!(env.render_ok("bar.substr(2, -1)"), @"");
4190 insta::assert_snapshot!(env.render_ok("bar.substr(4, -4)"), @"");
4191 insta::assert_snapshot!(env.render_ok("json(odd_case.substr(1, 2))"), @"[128]");
4192
4193 insta::assert_snapshot!(env.render_ok("'|' ++ empty.first_line() ++ '|'"), @"||");
4194 insta::assert_snapshot!(env.render_ok("'|' ++ foo_bar_nl.first_line() ++ '|'"), @"|foo|");
4195 insta::assert_snapshot!(env.render_ok("empty.lines().len()"), @"0");
4196 insta::assert_snapshot!(env.render_ok("foo_bar_nl.lines()"), @"foo bar");
4197 insta::assert_snapshot!(env.render_ok("json(odd.first_line())"), @"[128]");
4198 insta::assert_snapshot!(env.render_ok("json(odd.lines())"), @"[[128]]");
4199
4200 insta::assert_snapshot!(env.render_ok("empty.split(' ').join(',')"), @"");
4201 insta::assert_snapshot!(env.render_ok("foo_bar_case.split(' ').join(',')"), @"foo,BAR");
4202 insta::assert_snapshot!(env.render_ok("foo_bar_case.split(' ', 0).join(',')"), @"");
4203 insta::assert_snapshot!(env.render_ok("foo_bar_case.split(' ', 1).join(',')"), @"foo BAR");
4204 insta::assert_snapshot!(env.render_ok("foo_bar_case.split(' ', 2).join(',')"), @"foo,BAR");
4205 insta::assert_snapshot!(
4206 env.render_ok("json(odd_case.split(regex:'(?-u)[^Az]'))"), @"[[65],[122]]");
4207 insta::assert_snapshot!(env.render_ok("json(odd_case.split(regex:'A'))"), @"[[],[128,122]]");
4208
4209 insta::assert_snapshot!(env.render_ok("foo.replace('o', '*')"), @"f**");
4210 insta::assert_snapshot!(env.render_ok("foo.replace('o', '*', 0)"), @"foo");
4211 insta::assert_snapshot!(env.render_ok("foo.replace('o', '*', 1)"), @"f*o");
4212 insta::assert_snapshot!(env.render_ok("foo.replace('o', '*', 2)"), @"f**");
4213 insta::assert_snapshot!(env.render_ok("bar.replace(regex:'b(a)', '$0$1')"), @"baar");
4214 insta::assert_snapshot!(env.render_ok("json(foo.replace('o', odd))"), @"[102,128,128]");
4215 insta::assert_snapshot!(
4216 env.render_ok("json(odd_case.replace(regex:'(?-u)[^Az]', ' '))"), @"[65,32,122]");
4217 insta::assert_snapshot!(
4218 env.render_ok("json(odd_case.replace(regex:'A', ' '))"), @"[32,128,122]");
4219
4220 insta::assert_snapshot!(env.render_ok("foo_bar_case.upper()"), @"FOO BAR");
4221 insta::assert_snapshot!(env.render_ok("foo_bar_case.lower()"), @"foo bar");
4222 insta::assert_snapshot!(env.render_ok("json(odd_case.upper())"), @"[65,128,90]");
4223 insta::assert_snapshot!(env.render_ok("json(odd_case.lower())"), @"[97,128,122]");
4224 }
4225
4226 #[test]
4227 fn test_string_method() {
4228 let mut env = TestTemplateEnv::new();
4229 env.add_keyword("description", || literal("description 1".to_owned()));
4230 env.add_keyword("bad_string", || new_error_property::<String>("Bad"));
4231
4232 insta::assert_snapshot!(env.render_ok(r#""".len()"#), @"0");
4233 insta::assert_snapshot!(env.render_ok(r#""foo".len()"#), @"3");
4234 insta::assert_snapshot!(env.render_ok(r#""💩".len()"#), @"4");
4235
4236 insta::assert_snapshot!(env.render_ok(r#""fooo".contains("foo")"#), @"true");
4237 insta::assert_snapshot!(env.render_ok(r#""foo".contains("fooo")"#), @"false");
4238 insta::assert_snapshot!(env.render_ok(r#"description.contains("description")"#), @"true");
4239 insta::assert_snapshot!(
4240 env.render_ok(r#""description 123".contains(description.first_line())"#),
4241 @"true");
4242
4243 insta::assert_snapshot!(env.parse_err(r#""fa".starts_with(regex:'[a-f]o+')"#), @r#"
4245 --> 1:18
4246 |
4247 1 | "fa".starts_with(regex:'[a-f]o+')
4248 | ^-------------^
4249 |
4250 = String patterns may not be used as expression values
4251 "#);
4252
4253 insta::assert_snapshot!(env.render_ok(r#""foo".contains(bad_string)"#), @"<Error: Bad>");
4255 insta::assert_snapshot!(
4256 env.render_ok(r#""foo".contains("f" ++ bad_string) ++ "bar""#), @"<Error: Bad>bar");
4257 insta::assert_snapshot!(
4258 env.render_ok(r#""foo".contains(separate("o", "f", bad_string))"#), @"<Error: Bad>");
4259
4260 insta::assert_snapshot!(env.render_ok(r#""fooo".match(regex:'[a-f]o+')"#), @"fooo");
4261 insta::assert_snapshot!(env.render_ok(r#""fa".match(regex:'[a-f]o+')"#), @"");
4262 insta::assert_snapshot!(env.render_ok(r#""hello".match(regex:"h(ell)o")"#), @"hello");
4263 insta::assert_snapshot!(env.render_ok(r#""HEllo".match(regex-i:"h(ell)o")"#), @"HEllo");
4264 insta::assert_snapshot!(env.render_ok(r#""hEllo".match(glob:"h*o")"#), @"hEllo");
4265 insta::assert_snapshot!(env.render_ok(r#""Hello".match(glob:"h*o")"#), @"");
4266 insta::assert_snapshot!(env.render_ok(r#""HEllo".match(glob-i:"h*o")"#), @"HEllo");
4267 insta::assert_snapshot!(env.render_ok(r#""hello".match("he")"#), @"he");
4268 insta::assert_snapshot!(env.render_ok(r#""hello".match(substring:"he")"#), @"he");
4269 insta::assert_snapshot!(env.render_ok(r#""hello".match(exact:"he")"#), @"");
4270
4271 insta::assert_snapshot!(env.render_ok(r#""🥺".match(regex:'(?-u)^(?:.)')"#), @"<Error: incomplete utf-8 byte sequence from index 0>");
4275
4276 insta::assert_snapshot!(env.parse_err(r#""hello".match(false)"#), @r#"
4277 --> 1:15
4278 |
4279 1 | "hello".match(false)
4280 | ^---^
4281 |
4282 = Expected string pattern
4283 "#);
4284 insta::assert_snapshot!(env.parse_err(r#""🥺".match(not-a-pattern:"abc")"#), @r#"
4285 --> 1:11
4286 |
4287 1 | "🥺".match(not-a-pattern:"abc")
4288 | ^-----------------^
4289 |
4290 = Bad string pattern
4291 Invalid string pattern kind `not-a-pattern:`
4292 "#);
4293
4294 insta::assert_snapshot!(env.render_ok(r#""".first_line()"#), @"");
4295 insta::assert_snapshot!(env.render_ok(r#""foo\nbar".first_line()"#), @"foo");
4296
4297 insta::assert_snapshot!(env.render_ok(r#""".lines()"#), @"");
4298 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc\n".lines()"#), @"a b c");
4299
4300 insta::assert_snapshot!(env.render_ok(r#""".split(",")"#), @"");
4301 insta::assert_snapshot!(env.render_ok(r#""a,b,c".split(",")"#), @"a b c");
4302 insta::assert_snapshot!(env.render_ok(r#""a::b::c::d".split("::")"#), @"a b c d");
4303 insta::assert_snapshot!(env.render_ok(r#""a,b,c,d".split(",", 0)"#), @"");
4304 insta::assert_snapshot!(env.render_ok(r#""a,b,c,d".split(",", 2)"#), @"a b,c,d");
4305 insta::assert_snapshot!(env.render_ok(r#""a,b,c,d".split(",", 3)"#), @"a b c,d");
4306 insta::assert_snapshot!(env.render_ok(r#""a,b,c,d".split(",", 10)"#), @"a b c d");
4307 insta::assert_snapshot!(env.render_ok(r#""abc".split(",", -1)"#), @"<Error: out of range integral type conversion attempted>");
4308 insta::assert_snapshot!(env.render_ok(r#"json("a1b2c3".split(regex:'\d+'))"#), @r#"["a","b","c",""]"#);
4309 insta::assert_snapshot!(env.render_ok(r#""foo bar baz".split(regex:'\s+')"#), @"foo bar baz");
4310 insta::assert_snapshot!(env.render_ok(r#""a1b2c3d4".split(regex:'\d+', 3)"#), @"a b c3d4");
4311 insta::assert_snapshot!(env.render_ok(r#"json("hello world".split(regex-i:"WORLD"))"#), @r#"["hello ",""]"#);
4312
4313 insta::assert_snapshot!(env.render_ok("''.upper()"), @"");
4314 insta::assert_snapshot!(env.render_ok("'ABCabc 123!@#'.upper()"), @"ABCABC 123!@#");
4315 insta::assert_snapshot!(env.render_ok("''.lower()"), @"");
4316 insta::assert_snapshot!(env.render_ok("'ABCabc 123!@#'.lower()"), @"abcabc 123!@#");
4317
4318 insta::assert_snapshot!(env.render_ok(r#""".starts_with("")"#), @"true");
4319 insta::assert_snapshot!(env.render_ok(r#""everything".starts_with("")"#), @"true");
4320 insta::assert_snapshot!(env.render_ok(r#""".starts_with("foo")"#), @"false");
4321 insta::assert_snapshot!(env.render_ok(r#""foo".starts_with("foo")"#), @"true");
4322 insta::assert_snapshot!(env.render_ok(r#""foobar".starts_with("foo")"#), @"true");
4323 insta::assert_snapshot!(env.render_ok(r#""foobar".starts_with("bar")"#), @"false");
4324
4325 insta::assert_snapshot!(env.render_ok(r#""".ends_with("")"#), @"true");
4326 insta::assert_snapshot!(env.render_ok(r#""everything".ends_with("")"#), @"true");
4327 insta::assert_snapshot!(env.render_ok(r#""".ends_with("foo")"#), @"false");
4328 insta::assert_snapshot!(env.render_ok(r#""foo".ends_with("foo")"#), @"true");
4329 insta::assert_snapshot!(env.render_ok(r#""foobar".ends_with("foo")"#), @"false");
4330 insta::assert_snapshot!(env.render_ok(r#""foobar".ends_with("bar")"#), @"true");
4331
4332 insta::assert_snapshot!(env.render_ok(r#""".remove_prefix("wip: ")"#), @"");
4333 insta::assert_snapshot!(
4334 env.render_ok(r#""wip: testing".remove_prefix("wip: ")"#),
4335 @"testing");
4336
4337 insta::assert_snapshot!(
4338 env.render_ok(r#""bar@my.example.com".remove_suffix("@other.example.com")"#),
4339 @"bar@my.example.com");
4340 insta::assert_snapshot!(
4341 env.render_ok(r#""bar@other.example.com".remove_suffix("@other.example.com")"#),
4342 @"bar");
4343
4344 insta::assert_snapshot!(env.render_ok(r#"" \n \r \t \r ".trim()"#), @"");
4345 insta::assert_snapshot!(env.render_ok(r#"" \n \r foo bar \t \r ".trim()"#), @"foo bar");
4346
4347 insta::assert_snapshot!(env.render_ok(r#"" \n \r \t \r ".trim_start()"#), @"");
4348 insta::assert_snapshot!(env.render_ok(r#"" \n \r foo bar \t \r ".trim_start()"#), @"foo bar");
4349
4350 insta::assert_snapshot!(env.render_ok(r#"" \n \r \t \r ".trim_end()"#), @"");
4351 insta::assert_snapshot!(env.render_ok(r#"" \n \r foo bar \t \r ".trim_end()"#), @"\n\r foo bar");
4352
4353 insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 0)"#), @"");
4354 insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 1)"#), @"f");
4355 insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 3)"#), @"foo");
4356 insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 4)"#), @"foo");
4357 insta::assert_snapshot!(env.render_ok(r#""foo".substr(1, 3)"#), @"oo");
4358 insta::assert_snapshot!(env.render_ok(r#""foo".substr(1)"#), @"oo");
4359 insta::assert_snapshot!(env.render_ok(r#""foo".substr(0)"#), @"foo");
4360 insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(2, -1)"#), @"cde");
4361 insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-3, 99)"#), @"def");
4362 insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-3)"#), @"def");
4363 insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-6, 99)"#), @"abcdef");
4364 insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-7, 1)"#), @"a");
4365 insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-100)"#), @"abcdef");
4366
4367 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(2, -1)"#), @"c💩");
4369 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, -3)"#), @"💩");
4370 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, -4)"#), @"");
4371 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(6, -3)"#), @"💩");
4372 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(7, -3)"#), @"");
4373 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, 4)"#), @"");
4374 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, 6)"#), @"");
4375 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, 7)"#), @"💩");
4376 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-1, 7)"#), @"");
4377 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-3, 7)"#), @"");
4378 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-4, 7)"#), @"💩");
4379 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(0)"#), @"abc💩");
4380 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(1)"#), @"bc💩");
4381 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3)"#), @"💩");
4382 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(4)"#), @"💩");
4383 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-5)"#), @"c💩");
4384 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-4)"#), @"💩");
4385 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-3)"#), @"");
4386 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-2)"#), @"");
4387 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-1)"#), @"");
4388
4389 insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(4, 2)"#), @"");
4391 insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-2, -4)"#), @"");
4392
4393 insta::assert_snapshot!(env.render_ok(r#""hello".escape_json()"#), @r#""hello""#);
4394 insta::assert_snapshot!(env.render_ok(r#""he \n ll \n \" o".escape_json()"#), @r#""he \n ll \n \" o""#);
4395
4396 insta::assert_snapshot!(env.render_ok(r#""hello world".replace("world", "jj")"#), @"hello jj");
4398 insta::assert_snapshot!(env.render_ok(r#""hello world world".replace("world", "jj")"#), @"hello jj jj");
4399 insta::assert_snapshot!(env.render_ok(r#""hello".replace("missing", "jj")"#), @"hello");
4400
4401 insta::assert_snapshot!(env.render_ok(r#""hello world world".replace("world", "jj", 0)"#), @"hello world world");
4403 insta::assert_snapshot!(env.render_ok(r#""hello world world".replace("world", "jj", 1)"#), @"hello jj world");
4404 insta::assert_snapshot!(env.render_ok(r#""hello world world world".replace("world", "jj", 2)"#), @"hello jj jj world");
4405
4406 insta::assert_snapshot!(env.render_ok(r#""hello world world".replace("world", "jj", -1)"#), @"<Error: out of range integral type conversion attempted>");
4408 insta::assert_snapshot!(env.render_ok(r#""hello world world".replace("world", "jj", -5)"#), @"<Error: out of range integral type conversion attempted>");
4409
4410 insta::assert_snapshot!(env.render_ok(r#""hello123world456".replace(regex:'\d+', "X")"#), @"helloXworldX");
4412 insta::assert_snapshot!(env.render_ok(r#""hello123world456".replace(regex:'\d+', "X", 1)"#), @"helloXworld456");
4413
4414 insta::assert_snapshot!(env.render_ok(r#""HELLO WORLD".replace(regex-i:"(hello) +(world)", "$2 $1")"#), @"WORLD HELLO");
4416 insta::assert_snapshot!(env.render_ok(r#""abc123".replace(regex:"([a-z]+)([0-9]+)", "$2-$1")"#), @"123-abc");
4417 insta::assert_snapshot!(env.render_ok(r#""foo123bar".replace(regex:'\d+', "[$0]")"#), @"foo[123]bar");
4418
4419 insta::assert_snapshot!(env.render_ok(r#""Hello World".replace(regex-i:"hello", "hi")"#), @"hi World");
4421 insta::assert_snapshot!(env.render_ok(r#""Hello World Hello".replace(regex-i:"hello", "hi")"#), @"hi World hi");
4422 insta::assert_snapshot!(env.render_ok(r#""Hello World Hello".replace(regex-i:"hello", "hi", 1)"#), @"hi World Hello");
4423
4424 insta::assert_snapshot!(env.render_ok(r#"'hello\d+world'.replace('\d+', "X")"#), @"helloXworld");
4426 insta::assert_snapshot!(env.render_ok(r#""(foo)($1)bar".replace("$1", "$2")"#), @"(foo)()bar");
4427 insta::assert_snapshot!(env.render_ok(r#""test(abc)end".replace("(abc)", "X")"#), @"testXend");
4428
4429 insta::assert_snapshot!(env.render_ok(r#""hello world".replace("world", description.first_line())"#), @"hello description 1");
4431
4432 insta::assert_snapshot!(env.render_ok(r#""hello world".replace("world", bad_string)"#), @"<Error: Bad>");
4434 }
4435
4436 #[test]
4437 fn test_config_value_method() {
4438 let mut env = TestTemplateEnv::new();
4439 env.add_keyword("boolean", || literal(ConfigValue::from(true)));
4440 env.add_keyword("integer", || literal(ConfigValue::from(42)));
4441 env.add_keyword("string", || literal(ConfigValue::from("foo")));
4442 env.add_keyword("string_list", || {
4443 literal(ConfigValue::from_iter(["foo", "bar"]))
4444 });
4445
4446 insta::assert_snapshot!(env.render_ok("boolean"), @"true");
4447 insta::assert_snapshot!(env.render_ok("integer"), @"42");
4448 insta::assert_snapshot!(env.render_ok("string"), @r#""foo""#);
4449 insta::assert_snapshot!(env.render_ok("string_list"), @r#"["foo", "bar"]"#);
4450
4451 insta::assert_snapshot!(env.render_ok("boolean.as_boolean()"), @"true");
4452 insta::assert_snapshot!(env.render_ok("integer.as_integer()"), @"42");
4453 insta::assert_snapshot!(env.render_ok("string.as_string()"), @"foo");
4454 insta::assert_snapshot!(env.render_ok("string_list.as_string_list()"), @"foo bar");
4455
4456 insta::assert_snapshot!(
4457 env.render_ok("boolean.as_integer()"),
4458 @"<Error: invalid type: boolean `true`, expected i64>");
4459 insta::assert_snapshot!(
4460 env.render_ok("integer.as_string()"),
4461 @"<Error: invalid type: integer `42`, expected a string>");
4462 insta::assert_snapshot!(
4463 env.render_ok("string.as_string_list()"),
4464 @r#"<Error: invalid type: string "foo", expected a sequence>"#);
4465 insta::assert_snapshot!(
4466 env.render_ok("string_list.as_boolean()"),
4467 @"<Error: invalid type: sequence, expected a boolean>");
4468 }
4469
4470 #[test]
4471 fn test_signature_and_email_methods() {
4472 let mut env = TestTemplateEnv::new();
4473
4474 env.add_keyword("author", || {
4475 literal(new_signature("Test User", "test.user@example.com"))
4476 });
4477 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user@example.com>");
4478 insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Test User");
4479 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@example.com");
4480 insta::assert_snapshot!(env.render_ok("author.email().local()"), @"test.user");
4481 insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"example.com");
4482 insta::assert_snapshot!(env.render_ok("author.timestamp()"), @"1970-01-01 00:00:00.000 +00:00");
4483
4484 env.add_keyword("author", || {
4485 literal(new_signature("Another Test User", "test.user@example.com"))
4486 });
4487 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Another Test User <test.user@example.com>");
4488 insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Another Test User");
4489 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@example.com");
4490
4491 env.add_keyword("author", || {
4492 literal(new_signature("Test User", "test.user@invalid@example.com"))
4493 });
4494 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user@invalid@example.com>");
4495 insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Test User");
4496 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@invalid@example.com");
4497 insta::assert_snapshot!(env.render_ok("author.email().local()"), @"test.user");
4498 insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"invalid@example.com");
4499
4500 env.add_keyword("author", || {
4501 literal(new_signature("Test User", "test.user"))
4502 });
4503 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user>");
4504 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user");
4505 insta::assert_snapshot!(env.render_ok("author.email().local()"), @"test.user");
4506 insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"");
4507
4508 env.add_keyword("author", || {
4509 literal(new_signature("Test User", "test.user+tag@example.com"))
4510 });
4511 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user+tag@example.com>");
4512 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user+tag@example.com");
4513 insta::assert_snapshot!(env.render_ok("author.email().local()"), @"test.user+tag");
4514 insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"example.com");
4515
4516 env.add_keyword("author", || literal(new_signature("Test User", "x@y")));
4517 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <x@y>");
4518 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"x@y");
4519 insta::assert_snapshot!(env.render_ok("author.email().local()"), @"x");
4520 insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"y");
4521
4522 env.add_keyword("author", || {
4523 literal(new_signature("", "test.user@example.com"))
4524 });
4525 insta::assert_snapshot!(env.render_ok(r#"author"#), @"<test.user@example.com>");
4526 insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"");
4527 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@example.com");
4528
4529 env.add_keyword("author", || literal(new_signature("Test User", "")));
4530 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User");
4531 insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Test User");
4532 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"");
4533 insta::assert_snapshot!(env.render_ok("author.email().local()"), @"");
4534 insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"");
4535
4536 env.add_keyword("author", || literal(new_signature("", "")));
4537 insta::assert_snapshot!(env.render_ok(r#"author"#), @"");
4538 insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"");
4539 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"");
4540 }
4541
4542 #[test]
4543 fn test_size_hint_method() {
4544 let mut env = TestTemplateEnv::new();
4545
4546 env.add_keyword("unbounded", || literal((5, None)));
4547 insta::assert_snapshot!(env.render_ok(r#"unbounded.lower()"#), @"5");
4548 insta::assert_snapshot!(env.render_ok(r#"unbounded.upper()"#), @"");
4549 insta::assert_snapshot!(env.render_ok(r#"unbounded.exact()"#), @"");
4550 insta::assert_snapshot!(env.render_ok(r#"unbounded.zero()"#), @"false");
4551
4552 env.add_keyword("bounded", || literal((0, Some(10))));
4553 insta::assert_snapshot!(env.render_ok(r#"bounded.lower()"#), @"0");
4554 insta::assert_snapshot!(env.render_ok(r#"bounded.upper()"#), @"10");
4555 insta::assert_snapshot!(env.render_ok(r#"bounded.exact()"#), @"");
4556 insta::assert_snapshot!(env.render_ok(r#"bounded.zero()"#), @"false");
4557
4558 env.add_keyword("zero", || literal((0, Some(0))));
4559 insta::assert_snapshot!(env.render_ok(r#"zero.lower()"#), @"0");
4560 insta::assert_snapshot!(env.render_ok(r#"zero.upper()"#), @"0");
4561 insta::assert_snapshot!(env.render_ok(r#"zero.exact()"#), @"0");
4562 insta::assert_snapshot!(env.render_ok(r#"zero.zero()"#), @"true");
4563 }
4564
4565 #[test]
4566 fn test_timestamp_method() {
4567 let mut env = TestTemplateEnv::new();
4568 env.add_keyword("now", || literal(Timestamp::now()));
4569 env.add_keyword("t0", || literal(new_timestamp(0, 0)));
4570 env.add_keyword("t0_plus1", || literal(new_timestamp(0, 60)));
4571 env.add_keyword("tmax", || literal(new_timestamp(i64::MAX, 0)));
4572
4573 insta::assert_snapshot!(env.render_ok("tmax"),
4575 @"<Error: Out-of-range date>");
4576
4577 insta::assert_snapshot!(
4578 env.render_ok(r#"t0.format("%Y%m%d %H:%M:%S")"#),
4579 @"19700101 00:00:00");
4580
4581 insta::assert_snapshot!(env.parse_err(r#"t0.format("%_")"#), @r#"
4583 --> 1:11
4584 |
4585 1 | t0.format("%_")
4586 | ^--^
4587 |
4588 = Invalid time format
4589 "#);
4590
4591 env.add_dynamic_keyword("good_dyn_format", || "%Y%m".to_owned());
4593 env.add_dynamic_keyword("bad_dyn_format", || "%_".to_owned());
4594 insta::assert_snapshot!(env.render_ok("t0.format(good_dyn_format)"), @"197001");
4595 insta::assert_snapshot!(
4596 env.render_ok("t0.format(bad_dyn_format)"),
4597 @"<Error: Invalid time format: %_>");
4598
4599 env.add_alias("time_format", r#""%Y-%m-%d""#);
4601 env.add_alias("bad_time_format", r#""%_""#);
4602 insta::assert_snapshot!(env.render_ok(r#"t0.format(time_format)"#), @"1970-01-01");
4603 insta::assert_snapshot!(env.parse_err(r#"t0.format(bad_time_format)"#), @r#"
4604 --> 1:11
4605 |
4606 1 | t0.format(bad_time_format)
4607 | ^-------------^
4608 |
4609 = In alias `bad_time_format`
4610 --> 1:1
4611 |
4612 1 | "%_"
4613 | ^--^
4614 |
4615 = Invalid time format
4616 "#);
4617
4618 insta::assert_snapshot!(env.render_ok("t0_plus1.utc()"), @"1970-01-01 00:00:00.000 +00:00");
4619
4620 assert!(!env.render_ok("now.ago()").is_empty());
4623 assert!(!env.render_ok("now.local()").is_empty());
4624
4625 insta::assert_snapshot!(env.render_ok("t0.after('1969')"), @"true");
4626 insta::assert_snapshot!(env.render_ok("t0.before('1969')"), @"false");
4627 insta::assert_snapshot!(env.render_ok("t0.after('now')"), @"false");
4628 insta::assert_snapshot!(env.render_ok("t0.before('now')"), @"true");
4629 insta::assert_snapshot!(env.parse_err("t0.before('invalid')"), @"
4630 --> 1:11
4631 |
4632 1 | t0.before('invalid')
4633 | ^-------^
4634 |
4635 = Invalid date pattern
4636 expected unsupported identifier as position 0..7
4637 ");
4638 insta::assert_snapshot!(env.parse_err("t0.before('invalid')"), @"
4639 --> 1:11
4640 |
4641 1 | t0.before('invalid')
4642 | ^-------^
4643 |
4644 = Invalid date pattern
4645 expected unsupported identifier as position 0..7
4646 ");
4647
4648 insta::assert_snapshot!(env.parse_err("t0.after(t0)"), @"
4650 --> 1:10
4651 |
4652 1 | t0.after(t0)
4653 | ^^
4654 |
4655 = Expected string literal
4656 ");
4657 insta::assert_snapshot!(env.parse_err("t0.before(t0)"), @"
4658 --> 1:11
4659 |
4660 1 | t0.before(t0)
4661 | ^^
4662 |
4663 = Expected string literal
4664 ");
4665
4666 insta::assert_snapshot!(env.render_ok("t0.since(t0_plus1)"), @"1970-01-01 01:00:00.000 +01:00 - 1970-01-01 00:00:00.000 +00:00");
4667 insta::assert_snapshot!(env.render_ok("t0_plus1.since(t0)"), @"1970-01-01 00:00:00.000 +00:00 - 1970-01-01 01:00:00.000 +01:00");
4668 insta::assert_snapshot!(env.parse_err("t0.since(false)"), @"
4669 --> 1:10
4670 |
4671 1 | t0.since(false)
4672 | ^---^
4673 |
4674 = Expected expression of type `Timestamp`, but actual type is `Boolean`
4675 ");
4676 }
4677
4678 #[test]
4679 fn test_timestamp_range_method() {
4680 let mut env = TestTemplateEnv::new();
4681 env.add_keyword("instant", || {
4682 literal(TimestampRange {
4683 start: new_timestamp(0, 0),
4684 end: new_timestamp(0, 0),
4685 })
4686 });
4687 env.add_keyword("one_msec", || {
4688 literal(TimestampRange {
4689 start: new_timestamp(0, 0),
4690 end: new_timestamp(1, -60),
4691 })
4692 });
4693
4694 insta::assert_snapshot!(
4695 env.render_ok("instant.start().format('%Y%m%d %H:%M:%S %Z')"),
4696 @"19700101 00:00:00 +00:00");
4697 insta::assert_snapshot!(
4698 env.render_ok("one_msec.end().format('%Y%m%d %H:%M:%S %Z')"),
4699 @"19691231 23:00:00 -01:00");
4700
4701 insta::assert_snapshot!(
4702 env.render_ok("instant.duration()"), @"less than a microsecond");
4703 insta::assert_snapshot!(
4704 env.render_ok("one_msec.duration()"), @"1 millisecond");
4705 }
4706
4707 #[test]
4708 fn test_fill_function() {
4709 let mut env = TestTemplateEnv::new();
4710 env.add_color("error", crossterm::style::Color::DarkRed);
4711
4712 insta::assert_snapshot!(
4713 env.render_ok(r#"fill(20, "The quick fox jumps over the " ++
4714 label("error", "lazy") ++ " dog\n")"#),
4715 @"
4716 The quick fox jumps
4717 over the [38;5;1mlazy[39m dog
4718 ");
4719
4720 insta::assert_snapshot!(
4722 env.render_ok(r#"fill(9, "Longlonglongword an some short words " ++
4723 label("error", "longlonglongword and short words") ++
4724 " back out\n")"#),
4725 @"
4726 Longlonglongword
4727 an some
4728 short
4729 words
4730 [38;5;1mlonglonglongword[39m
4731 [38;5;1mand short[39m
4732 [38;5;1mwords[39m
4733 back out
4734 ");
4735
4736 insta::assert_snapshot!(
4738 env.render_ok(r#"fill(0, "The quick fox jumps over the " ++
4739 label("error", "lazy") ++ " dog\n")"#),
4740 @"
4741 The
4742 quick
4743 fox
4744 jumps
4745 over
4746 the
4747 [38;5;1mlazy[39m
4748 dog
4749 ");
4750
4751 insta::assert_snapshot!(
4753 env.render_ok(r#"fill(-0, "The quick fox jumps over the " ++
4754 label("error", "lazy") ++ " dog\n")"#),
4755 @"
4756 The
4757 quick
4758 fox
4759 jumps
4760 over
4761 the
4762 [38;5;1mlazy[39m
4763 dog
4764 ");
4765
4766 insta::assert_snapshot!(
4768 env.render_ok(r#"fill(-10, "The quick fox jumps over the " ++
4769 label("error", "lazy") ++ " dog\n")"#),
4770 @"[38;5;1m<Error: out of range integral type conversion attempted>[39m");
4771
4772 insta::assert_snapshot!(
4774 env.render_ok(r#""START marker to help insta\n" ++
4775 indent(" ", fill(20, "The quick fox jumps over the " ++
4776 label("error", "lazy") ++ " dog\n"))"#),
4777 @"
4778 START marker to help insta
4779 The quick fox jumps
4780 over the [38;5;1mlazy[39m dog
4781 ");
4782
4783 insta::assert_snapshot!(
4785 env.render_ok(r#""START marker to help insta\n" ++
4786 fill(20, indent(" ", "The quick fox jumps over the " ++
4787 label("error", "lazy") ++ " dog\n"))"#),
4788 @"
4789 START marker to help insta
4790 The quick fox
4791 jumps over the [38;5;1mlazy[39m
4792 dog
4793 ");
4794 }
4795
4796 #[test]
4797 fn test_replace_function() {
4798 let mut env = TestTemplateEnv::new();
4799 env.add_color("error", crossterm::style::Color::DarkRed);
4800 env.add_color("warning", crossterm::style::Color::DarkYellow);
4801
4802 insta::assert_snapshot!(
4804 env.render_ok(r#"replace("Hi", label("error", "Hi world"), |_| "Hello")"#),
4805 @"[38;5;1mHello world[39m");
4806
4807 insta::assert_snapshot!(
4809 env.render_ok(r#"replace("Hi", label("error", "Hi Hi world"), |_| "Hello")"#),
4810 @"[38;5;1mHello Hello world[39m");
4811
4812 insta::assert_snapshot!(
4815 env.render_ok(r#"replace("ello w",
4816 label("error", "Hello") ++ " " ++ label("warning", "world"),
4817 |c| c.get(0).upper())"#),
4818 @"[38;5;1mHELLO W[38;5;3morld[39m");
4819
4820 insta::assert_snapshot!(
4822 env.render_ok(r#"replace(regex-i:"(ello) (w)",
4823 label("error", "HELLO") ++ " " ++ label("warning", "world"),
4824 |c| c.get(1).lower() ++ " " ++ c.get(2).upper())"#),
4825 @"[38;5;1mHello W[38;5;3morld[39m");
4826
4827 insta::assert_snapshot!(
4829 env.render_ok(r#"replace(regex-i:'(?P<h>ello) (?P<w>w)',
4830 label("error", "HELLO") ++ " " ++ label("warning", "world"),
4831 |c| c.name("h").lower() ++ " " ++ c.name("w").upper())"#),
4832 @"[38;5;1mHello W[38;5;3morld[39m");
4833
4834 insta::assert_snapshot!(
4836 env.render_ok(r#"replace(regex-i:"(hello) (b)?",
4837 label("error", "HELLO") ++ " " ++ label("warning", "world"),
4838 |c| c.get(1).lower() ++ " " ++ c.get(2).upper())"#),
4839 @"[38;5;1mhello [38;5;3mworld[39m");
4840
4841 insta::assert_snapshot!(
4843 env.render_ok(r#"replace(regex-i:'(?P<h>ello) (?P<b>b)',
4844 label("error", "HELLO") ++ " " ++ label("warning", "world"),
4845 |c| c.name("h").lower() ++ " " ++ c.name("b").upper())"#),
4846 @"[38;5;1mHELLO[39m [38;5;3mworld[39m");
4847
4848 insta::assert_snapshot!(
4850 env.render_ok(r#"replace(regex:"(a)(b)", "ab", |c| c.len())"#),
4851 @"3");
4852
4853 insta::assert_snapshot!(
4855 env.render_ok(r#"replace("Hi", "Hi world", |c| c.get(99))"#),
4856 @"[38;5;1m<Error: Could not get capture group with index 99>[39m world");
4857
4858 insta::assert_snapshot!(
4860 env.render_ok(r#"replace("Hi", "Hi world", |c| c.name("no_such_group"))"#),
4861 @"[38;5;1m<Error: Could not get capture group with name no_such_group>[39m world");
4862
4863 insta::assert_snapshot!(
4865 env.render_ok(r#"replace(regex:'(?-u)^(.{3})(.)$', "🥺", |c| json(c.get(1)))"#),
4866 @"[240,159,165]");
4867 }
4868
4869 #[test]
4870 fn test_indent_function() {
4871 let mut env = TestTemplateEnv::new();
4872 env.add_color("error", crossterm::style::Color::DarkRed);
4873 env.add_color("warning", crossterm::style::Color::DarkYellow);
4874 env.add_color("hint", crossterm::style::Color::DarkCyan);
4875
4876 assert_eq!(env.render_ok(r#"indent("__", "")"#), "");
4879 assert_eq!(env.render_ok(r#"indent("__", "\n")"#), "\n");
4880 assert_eq!(env.render_ok(r#"indent("__", "a\n\nb")"#), "__a\n\n__b");
4881
4882 insta::assert_snapshot!(
4884 env.render_ok(r#"indent("__", label("error", "a\n") ++ label("warning", "b\n"))"#),
4885 @"
4886 [38;5;1m__a[39m
4887 [38;5;3m__b[39m
4888 ");
4889
4890 insta::assert_snapshot!(
4892 env.render_ok(r#"indent("__", label("error", "a") ++ label("warning", "b\nc"))"#),
4893 @"
4894 [38;5;1m__a[38;5;3mb[39m
4895 [38;5;3m__c[39m
4896 ");
4897
4898 insta::assert_snapshot!(
4900 env.render_ok(r#"indent(label("error", "XX"), "a\nb\n")"#),
4901 @"
4902 [38;5;1mXX[39ma
4903 [38;5;1mXX[39mb
4904 ");
4905
4906 insta::assert_snapshot!(
4908 env.render_ok(r#"indent(label("hint", "A"),
4909 label("warning", indent(label("hint", "B"),
4910 label("error", "x\n") ++ "y")))"#),
4911 @"
4912 [38;5;6mAB[38;5;1mx[39m
4913 [38;5;6mAB[38;5;3my[39m
4914 ");
4915 }
4916
4917 #[test]
4918 fn test_pad_function() {
4919 let mut env = TestTemplateEnv::new();
4920 env.add_keyword("bad_string", || new_error_property::<String>("Bad"));
4921 env.add_color("red", crossterm::style::Color::Red);
4922 env.add_color("cyan", crossterm::style::Color::DarkCyan);
4923
4924 insta::assert_snapshot!(
4926 env.render_ok(r"'{' ++ pad_start(5, label('red', 'foo')) ++ '}'"),
4927 @"{ [38;5;9mfoo[39m}");
4928 insta::assert_snapshot!(
4929 env.render_ok(r"'{' ++ pad_end(5, label('red', 'foo')) ++ '}'"),
4930 @"{[38;5;9mfoo[39m }");
4931 insta::assert_snapshot!(
4932 env.render_ok(r"'{' ++ pad_centered(5, label('red', 'foo')) ++ '}'"),
4933 @"{ [38;5;9mfoo[39m }");
4934
4935 insta::assert_snapshot!(
4937 env.render_ok(r"pad_start(5, label('red', 'foo'), fill_char=label('cyan', '='))"),
4938 @"[38;5;6m==[38;5;9mfoo[39m");
4939 insta::assert_snapshot!(
4940 env.render_ok(r"pad_end(5, label('red', 'foo'), fill_char=label('cyan', '='))"),
4941 @"[38;5;9mfoo[38;5;6m==[39m");
4942 insta::assert_snapshot!(
4943 env.render_ok(r"pad_centered(5, label('red', 'foo'), fill_char=label('cyan', '='))"),
4944 @"[38;5;6m=[38;5;9mfoo[38;5;6m=[39m");
4945
4946 insta::assert_snapshot!(
4949 env.render_ok(r"pad_start(3, 'foo', fill_char=bad_string)"),
4950 @"foo");
4951 insta::assert_snapshot!(
4952 env.render_ok(r"pad_end(5, 'foo', fill_char=bad_string)"),
4953 @"foo<<Error: Error: Bad>Bad>");
4954 insta::assert_snapshot!(
4955 env.render_ok(r"pad_centered(5, 'foo', fill_char=bad_string)"),
4956 @"<Error: Bad>foo<Error: Bad>");
4957
4958 insta::assert_snapshot!(
4960 env.render_ok("pad_start(-1, 'foo')"),
4961 @"<Error: out of range integral type conversion attempted>");
4962 }
4963
4964 #[test]
4965 fn test_hash_function() {
4966 let mut env = TestTemplateEnv::new();
4967 env.add_color("red", crossterm::style::Color::Red);
4968
4969 assert_eq!(env.render_ok("hash(false)"), env.render_ok("hash('false')"));
4972 assert_eq!(env.render_ok("hash(0)"), env.render_ok("hash('0')"));
4973 assert_eq!(
4974 env.render_ok("hash(0)"),
4975 env.render_ok("hash(label('red', '0'))")
4976 );
4977 }
4978
4979 #[test]
4980 fn test_truncate_function() {
4981 let mut env = TestTemplateEnv::new();
4982 env.add_color("red", crossterm::style::Color::Red);
4983
4984 insta::assert_snapshot!(
4985 env.render_ok(r"truncate_start(2, label('red', 'foobar')) ++ 'baz'"),
4986 @"[38;5;9mar[39mbaz");
4987 insta::assert_snapshot!(
4988 env.render_ok("truncate_start(5, 'foo', 'bar')"), @"foo");
4989 insta::assert_snapshot!(
4990 env.render_ok("truncate_start(9, 'foobarbazquux', 'dotdot')"), @"dotdotuux");
4991
4992 insta::assert_snapshot!(
4993 env.render_ok(r"truncate_end(2, label('red', 'foobar')) ++ 'baz'"),
4994 @"[38;5;9mfo[39mbaz");
4995 insta::assert_snapshot!(
4996 env.render_ok("truncate_end(5, 'foo', 'bar')"), @"foo");
4997 insta::assert_snapshot!(
4998 env.render_ok("truncate_end(9, 'foobarbazquux', 'dotdot')"), @"foodotdot");
4999
5000 insta::assert_snapshot!(
5002 env.render_ok("truncate_end(-1, 'foo')"),
5003 @"<Error: out of range integral type conversion attempted>");
5004 }
5005
5006 #[test]
5007 fn test_label_function() {
5008 let mut env = TestTemplateEnv::new();
5009 env.add_keyword("empty", || literal(true));
5010 env.add_color("error", crossterm::style::Color::DarkRed);
5011 env.add_color("warning", crossterm::style::Color::DarkYellow);
5012
5013 insta::assert_snapshot!(
5015 env.render_ok(r#"label("error", "text")"#),
5016 @"[38;5;1mtext[39m");
5017
5018 insta::assert_snapshot!(
5020 env.render_ok(r#"label("error".first_line(), "text")"#),
5021 @"[38;5;1mtext[39m");
5022
5023 insta::assert_snapshot!(
5025 env.render_ok("label(fill(-1, 'foo'), 'text')"),
5026 @"[38;5;1m<Error: out of range integral type conversion attempted>[39m");
5027
5028 insta::assert_snapshot!(
5030 env.render_ok(r#"label(if(empty, "error", "warning"), "text")"#),
5031 @"[38;5;1mtext[39m");
5032 }
5033
5034 #[test]
5035 fn test_raw_escape_sequence_function_strip_labels() {
5036 let mut env = TestTemplateEnv::new();
5037 env.add_color("error", crossterm::style::Color::DarkRed);
5038 env.add_color("warning", crossterm::style::Color::DarkYellow);
5039
5040 insta::assert_snapshot!(
5041 env.render_ok(r#"raw_escape_sequence(label("error warning", "text"))"#),
5042 @"text",
5043 );
5044 }
5045
5046 #[test]
5047 fn test_raw_escape_sequence_function_ansi_escape() {
5048 let env = TestTemplateEnv::new();
5049
5050 insta::assert_snapshot!(env.render_ok(r#""\e""#), @"␛");
5052 insta::assert_snapshot!(env.render_ok(r#""\x1b""#), @"␛");
5053 insta::assert_snapshot!(env.render_ok(r#""\x1B""#), @"␛");
5054 insta::assert_snapshot!(
5055 env.render_ok(r#""]8;;"
5056 ++ "http://example.com"
5057 ++ "\e\\"
5058 ++ "Example"
5059 ++ "\x1b]8;;\x1B\\""#),
5060 @r"␛]8;;http://example.com␛\Example␛]8;;␛\");
5061
5062 insta::assert_snapshot!(env.render_ok(r#"raw_escape_sequence("\e")"#), @"");
5064 insta::assert_snapshot!(env.render_ok(r#"raw_escape_sequence("\x1b")"#), @"");
5065 insta::assert_snapshot!(env.render_ok(r#"raw_escape_sequence("\x1B")"#), @"");
5066 insta::assert_snapshot!(
5067 env.render_ok(r#"raw_escape_sequence("]8;;"
5068 ++ "http://example.com"
5069 ++ "\e\\"
5070 ++ "Example"
5071 ++ "\x1b]8;;\x1B\\")"#),
5072 @r"]8;;http://example.com\Example]8;;\");
5073 }
5074
5075 #[test]
5076 fn test_hyperlink_function_with_color() {
5077 let mut env = TestTemplateEnv::new();
5078 env.add_keyword("bad_string", || new_error_property::<String>("Bad"));
5079 insta::assert_snapshot!(
5081 env.render_ok(r#"hyperlink("http://example.com", "Example")"#),
5082 @r"]8;;http://example.com\Example]8;;\");
5083 insta::assert_snapshot!(
5084 env.render_ok(r#"hyperlink(bad_string, "Example")"#),
5085 @"<Error: Bad>");
5086 }
5087
5088 #[test]
5089 fn test_hyperlink_function_without_color() {
5090 let env = TestTemplateEnv::new();
5091 insta::assert_snapshot!(
5093 env.render_plain(r#"hyperlink("http://example.com", "Example")"#),
5094 @"Example");
5095 }
5096
5097 #[test]
5098 fn test_hyperlink_function_custom_fallback() {
5099 let env = TestTemplateEnv::new();
5100 insta::assert_snapshot!(
5102 env.render_plain(r#"hyperlink("http://example.com", "Example", "URL: http://example.com")"#),
5103 @"URL: http://example.com");
5104 }
5105
5106 #[test]
5107 fn test_hyperlink_function_stringify() {
5108 let env = TestTemplateEnv::new();
5109 insta::assert_snapshot!(
5111 env.render_ok(r#"stringify(hyperlink("http://example.com", "Example"))"#),
5112 @"Example");
5113 insta::assert_snapshot!(
5115 env.render_ok(r#"stringify(hyperlink("http://example.com", "Example")).upper()"#),
5116 @"EXAMPLE");
5117 }
5118
5119 #[test]
5120 fn test_hyperlink_function_with_separate() {
5121 let env = TestTemplateEnv::new();
5122 insta::assert_snapshot!(
5124 env.render_ok(r#"separate(" | ", hyperlink("http://a.com", "A"), hyperlink("http://b.com", "B"))"#),
5125 @r"]8;;http://a.com\A]8;;\ | ]8;;http://b.com\B]8;;\");
5126 }
5127
5128 #[test]
5129 fn test_hyperlink_function_with_coalesce() {
5130 let env = TestTemplateEnv::new();
5131 insta::assert_snapshot!(
5133 env.render_ok(r#"coalesce(hyperlink("http://example.com", "Link"), "fallback")"#),
5134 @r"]8;;http://example.com\Link]8;;\");
5135 insta::assert_snapshot!(
5137 env.render_ok(r#"coalesce(hyperlink("http://example.com", ""), "fallback")"#),
5138 @"fallback");
5139 }
5140
5141 #[test]
5142 fn test_hyperlink_function_with_if() {
5143 let env = TestTemplateEnv::new();
5144 insta::assert_snapshot!(
5146 env.render_ok(r#"if(true, hyperlink("http://example.com", "Yes"), "No")"#),
5147 @r"]8;;http://example.com\Yes]8;;\");
5148 insta::assert_snapshot!(
5149 env.render_ok(r#"if(false, "Yes", hyperlink("http://example.com", "No"))"#),
5150 @r"]8;;http://example.com\No]8;;\");
5151 }
5152
5153 #[test]
5154 fn test_hyperlink_function_plain_with_separate() {
5155 let env = TestTemplateEnv::new();
5156 insta::assert_snapshot!(
5158 env.render_plain(r#"separate(" | ", hyperlink("http://a.com", "A"), hyperlink("http://b.com", "B"))"#),
5159 @"A | B");
5160 }
5161
5162 #[test]
5163 fn test_stringify_function() {
5164 let mut env = TestTemplateEnv::new();
5165 env.add_keyword("none_i64", || literal(None::<i64>));
5166 env.add_keyword("ascii_bstr", || literal(BString::from("foo")));
5167 env.add_keyword("odd_bstr", || literal(BString::from(b"\x80")));
5168 env.add_color("error", crossterm::style::Color::DarkRed);
5169
5170 insta::assert_snapshot!(env.render_ok("stringify(false)"), @"false");
5171 insta::assert_snapshot!(env.render_ok("stringify(42).len()"), @"2");
5172 insta::assert_snapshot!(env.render_ok("stringify(none_i64)"), @"");
5173 insta::assert_snapshot!(env.render_ok("stringify(ascii_bstr)"), @"foo");
5174 insta::assert_snapshot!(
5175 env.render_ok("stringify(odd_bstr)"),
5176 @"[38;5;1m<Error: invalid utf-8 sequence of 1 bytes from index 0>[39m");
5177 insta::assert_snapshot!(env.render_ok("stringify(label('error', 'text'))"), @"text");
5178 }
5179
5180 #[test]
5181 fn test_json_function() {
5182 let mut env = TestTemplateEnv::new();
5183 env.add_keyword("none_i64", || literal(None::<i64>));
5184 env.add_keyword("ascii_bstr", || literal(BString::from("foo")));
5185 env.add_keyword("string_list", || {
5186 literal(vec!["foo".to_owned(), "bar".to_owned()])
5187 });
5188 env.add_keyword("config_value_table", || {
5189 literal(ConfigValue::from_iter([("foo", "bar")]))
5190 });
5191 env.add_keyword("some_cfgval", || literal(Some(ConfigValue::from(1))));
5192 env.add_keyword("none_cfgval", || literal(None::<ConfigValue>));
5193 env.add_keyword("signature", || {
5194 literal(Signature {
5195 name: "Test User".to_owned(),
5196 email: "test.user@example.com".to_owned(),
5197 timestamp: Timestamp {
5198 timestamp: MillisSinceEpoch(0),
5199 tz_offset: 0,
5200 },
5201 })
5202 });
5203 env.add_keyword("email", || literal(Email("foo@bar".to_owned())));
5204 env.add_keyword("size_hint", || literal((5, None)));
5205 env.add_keyword("timestamp", || {
5206 literal(Timestamp {
5207 timestamp: MillisSinceEpoch(0),
5208 tz_offset: 0,
5209 })
5210 });
5211 env.add_keyword("timestamp_range", || {
5212 literal(TimestampRange {
5213 start: Timestamp {
5214 timestamp: MillisSinceEpoch(0),
5215 tz_offset: 0,
5216 },
5217 end: Timestamp {
5218 timestamp: MillisSinceEpoch(86_400_000),
5219 tz_offset: -60,
5220 },
5221 })
5222 });
5223
5224 insta::assert_snapshot!(env.render_ok(r#"json(ascii_bstr)"#), @"[102,111,111]");
5225 insta::assert_snapshot!(env.render_ok(r#"json('"quoted"')"#), @r#""\"quoted\"""#);
5226 insta::assert_snapshot!(env.render_ok(r#"json(string_list)"#), @r#"["foo","bar"]"#);
5227 insta::assert_snapshot!(env.render_ok("json(false)"), @"false");
5228 insta::assert_snapshot!(env.render_ok("json(42)"), @"42");
5229 insta::assert_snapshot!(env.render_ok("json(none_i64)"), @"null");
5230 insta::assert_snapshot!(env.render_ok(r#"json(config_value_table)"#), @r#"{"foo":"bar"}"#);
5231 insta::assert_snapshot!(env.render_ok(r"json(some_cfgval)"), @"1");
5232 insta::assert_snapshot!(env.render_ok(r"json(none_cfgval)"), @"null");
5233 insta::assert_snapshot!(env.render_ok("json(email)"), @r#""foo@bar""#);
5234 insta::assert_snapshot!(
5235 env.render_ok("json(signature)"),
5236 @r#"{"name":"Test User","email":"test.user@example.com","timestamp":"1970-01-01T00:00:00Z"}"#);
5237 insta::assert_snapshot!(env.render_ok("json(size_hint)"), @"[5,null]");
5238 insta::assert_snapshot!(env.render_ok("json(timestamp)"), @r#""1970-01-01T00:00:00Z""#);
5239 insta::assert_snapshot!(
5240 env.render_ok("json(timestamp_range)"),
5241 @r#"{"start":"1970-01-01T00:00:00Z","end":"1970-01-01T23:00:00-01:00"}"#);
5242
5243 insta::assert_snapshot!(env.render_ok(r#"json(string_list.map(|s| s))"#), @r#"["foo","bar"]"#);
5245 insta::assert_snapshot!(env.render_ok(r#"json(string_list.map(|s| size_hint))"#), @"[[5,null],[5,null]]");
5246
5247 insta::assert_snapshot!(env.render_ok(r#"json(if(true, email, timestamp))"#), @r#""foo@bar""#);
5249 insta::assert_snapshot!(env.render_ok(r#"json(if(true, size_hint, config_value_table))"#), @"[5,null]");
5250
5251 insta::assert_snapshot!(env.parse_err(r#"json(if(true, email))"#), @r###"
5254 --> 1:6
5255 |
5256 1 | json(if(true, email))
5257 | ^-------------^
5258 |
5259 = Expected expression of type `Serialize`, but actual type is `Any`
5260 "###);
5261 insta::assert_snapshot!(env.parse_err(r#"json(if(false, email))"#), @r###"
5262 --> 1:6
5263 |
5264 1 | json(if(false, email))
5265 | ^--------------^
5266 |
5267 = Expected expression of type `Serialize`, but actual type is `Any`
5268 "###);
5269 }
5270
5271 #[test]
5272 fn test_coalesce_function() {
5273 let mut env = TestTemplateEnv::new();
5274 env.add_keyword("bad_string", || new_error_property::<String>("Bad"));
5275 env.add_keyword("empty_string", || literal("".to_owned()));
5276 env.add_keyword("non_empty_string", || literal("a".to_owned()));
5277
5278 insta::assert_snapshot!(env.render_ok(r#"coalesce()"#), @"");
5279 insta::assert_snapshot!(env.render_ok(r#"coalesce("")"#), @"");
5280 insta::assert_snapshot!(env.render_ok(r#"coalesce("", "a", "", "b")"#), @"a");
5281 insta::assert_snapshot!(
5282 env.render_ok(r#"coalesce(empty_string, "", non_empty_string)"#), @"a");
5283
5284 insta::assert_snapshot!(env.render_ok(r#"coalesce(false, true)"#), @"false");
5286
5287 insta::assert_snapshot!(env.render_ok(r#"coalesce(bad_string, "a")"#), @"<Error: Bad>");
5289 insta::assert_snapshot!(env.render_ok(r#"coalesce("a", bad_string)"#), @"a");
5291
5292 insta::assert_snapshot!(env.parse_err(r#"coalesce("a", value2="b")"#), @r#"
5294 --> 1:15
5295 |
5296 1 | coalesce("a", value2="b")
5297 | ^--------^
5298 |
5299 = Function `coalesce`: Unexpected keyword arguments
5300 "#);
5301 }
5302
5303 #[test]
5304 fn test_concat_function() {
5305 let mut env = TestTemplateEnv::new();
5306 env.add_keyword("empty", || literal(true));
5307 env.add_keyword("hidden", || literal(false));
5308 env.add_color("empty", crossterm::style::Color::DarkGreen);
5309 env.add_color("error", crossterm::style::Color::DarkRed);
5310 env.add_color("warning", crossterm::style::Color::DarkYellow);
5311
5312 insta::assert_snapshot!(env.render_ok(r#"concat()"#), @"");
5313 insta::assert_snapshot!(
5314 env.render_ok(r#"concat(hidden, empty)"#),
5315 @"false[38;5;2mtrue[39m");
5316 insta::assert_snapshot!(
5317 env.render_ok(r#"concat(label("error", ""), label("warning", "a"), "b")"#),
5318 @"[38;5;3ma[39mb");
5319
5320 insta::assert_snapshot!(env.parse_err(r#"concat("a", value2="b")"#), @r#"
5322 --> 1:13
5323 |
5324 1 | concat("a", value2="b")
5325 | ^--------^
5326 |
5327 = Function `concat`: Unexpected keyword arguments
5328 "#);
5329 }
5330
5331 #[test]
5332 fn test_join_function() {
5333 let mut env = TestTemplateEnv::new();
5334 env.add_keyword("description", || literal("".to_owned()));
5335 env.add_keyword("empty", || literal(true));
5336 env.add_keyword("hidden", || literal(false));
5337 env.add_color("empty", crossterm::style::Color::DarkGreen);
5338 env.add_color("error", crossterm::style::Color::DarkRed);
5339 env.add_color("warning", crossterm::style::Color::DarkYellow);
5340
5341 insta::assert_snapshot!(env.render_ok(r#"join(",")"#), @"");
5343 insta::assert_snapshot!(env.render_ok(r#"join(",", "")"#), @"");
5344 insta::assert_snapshot!(env.render_ok(r#"join(",", "a")"#), @"a");
5345 insta::assert_snapshot!(env.render_ok(r#"join(",", "a", "b")"#), @"a,b");
5346 insta::assert_snapshot!(env.render_ok(r#"join(",", "a", "", "b")"#), @"a,,b");
5347 insta::assert_snapshot!(env.render_ok(r#"join(",", "a", "b", "")"#), @"a,b,");
5348 insta::assert_snapshot!(env.render_ok(r#"join(",", "", "a", "b")"#), @",a,b");
5349 insta::assert_snapshot!(
5350 env.render_ok(r#"join("--", 1, "", true, "test", "")"#),
5351 @"1----true--test--");
5352
5353 insta::assert_snapshot!(env.parse_err(r#"join()"#), @"
5355 --> 1:6
5356 |
5357 1 | join()
5358 | ^
5359 |
5360 = Function `join`: Expected at least 1 arguments
5361 ");
5362
5363 insta::assert_snapshot!(
5365 env.render_ok(r#"join(",", label("error", ""), label("warning", "a"), "b")"#),
5366 @",[38;5;3ma[39m,b");
5367 insta::assert_snapshot!(
5368 env.render_ok(
5369 r#"join(label("empty", "<>"), label("error", "a"), label("warning", ""), "b")"#),
5370 @"[38;5;1ma[38;5;2m<><>[39mb");
5371
5372 insta::assert_snapshot!(env.render_ok(r#"join(",", "a", ("" ++ ""))"#), @"a,");
5374 insta::assert_snapshot!(env.render_ok(r#"join(",", "a", ("" ++ "b"))"#), @"a,b");
5375
5376 insta::assert_snapshot!(
5378 env.render_ok(r#"join(",", "a", join("|", "", ""))"#), @"a,|");
5379 insta::assert_snapshot!(
5380 env.render_ok(r#"join(",", "a", join("|", "b", ""))"#), @"a,b|");
5381 insta::assert_snapshot!(
5382 env.render_ok(r#"join(",", "a", join("|", "b", "c"))"#), @"a,b|c");
5383
5384 insta::assert_snapshot!(
5386 env.render_ok(r#"join(",", hidden, description, empty)"#),
5387 @"false,,[38;5;2mtrue[39m");
5388 insta::assert_snapshot!(
5389 env.render_ok(r#"join(hidden, "X", "Y", "Z")"#),
5390 @"XfalseYfalseZ");
5391 insta::assert_snapshot!(
5392 env.render_ok(r#"join(hidden, empty)"#),
5393 @"[38;5;2mtrue[39m");
5394
5395 insta::assert_snapshot!(env.parse_err(r#"join(",", "a", arg="b")"#), @r#"
5397 --> 1:16
5398 |
5399 1 | join(",", "a", arg="b")
5400 | ^-----^
5401 |
5402 = Function `join`: Unexpected keyword arguments
5403 "#);
5404
5405 env.add_keyword("str_list", || {
5407 literal(vec!["foo".to_owned(), "bar".to_owned()])
5408 });
5409 env.add_keyword("none_int", || literal(None::<i64>));
5410 env.add_keyword("some_int", || literal(Some(67)));
5411 env.add_keyword("cfg_val", || {
5412 literal(ConfigValue::from_iter([("foo", "bar")]))
5413 });
5414 env.add_keyword("email", || literal(Email("me@example.com".to_owned())));
5415 env.add_keyword("signature", || {
5416 literal(new_signature("User", "user@example.com"))
5417 });
5418 env.add_keyword("size_hint", || literal((10, None)));
5419 env.add_keyword("timestamp", || literal(new_timestamp(0, 0)));
5420 env.add_keyword("timestamp_range", || {
5421 literal(TimestampRange {
5422 start: new_timestamp(0, 0),
5423 end: new_timestamp(0, 0),
5424 })
5425 });
5426 insta::assert_snapshot!(
5427 env.render_ok("join('|', str_list, 42, none_int, some_int)"),
5428 @"foo bar|42||67");
5429 insta::assert_snapshot!(
5430 env.render_ok("join('|', cfg_val, email, signature, if(true, 42), if(false, 42))"),
5431 @r#"{ foo = "bar" }|me@example.com|User <user@example.com>|42|"#);
5432 insta::assert_snapshot!(
5433 env.render_ok("join('|', timestamp, timestamp_range, str_list.map(|x| x))"),
5434 @"1970-01-01 00:00:00.000 +00:00|1970-01-01 00:00:00.000 +00:00 - 1970-01-01 00:00:00.000 +00:00|foo bar");
5435 assert_matches!(
5436 env.parse_err_kind("join('|', size_hint)"),
5437 TemplateParseErrorKind::Expression(_)
5438 );
5439 }
5440
5441 #[test]
5442 fn test_separate_function() {
5443 let mut env = TestTemplateEnv::new();
5444 env.add_keyword("description", || literal("".to_owned()));
5445 env.add_keyword("empty", || literal(true));
5446 env.add_keyword("hidden", || literal(false));
5447 env.add_color("empty", crossterm::style::Color::DarkGreen);
5448 env.add_color("error", crossterm::style::Color::DarkRed);
5449 env.add_color("warning", crossterm::style::Color::DarkYellow);
5450
5451 insta::assert_snapshot!(env.render_ok(r#"separate(" ")"#), @"");
5452 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "")"#), @"");
5453 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a")"#), @"a");
5454 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", "b")"#), @"a b");
5455 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", "", "b")"#), @"a b");
5456 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", "b", "")"#), @"a b");
5457 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "", "a", "b")"#), @"a b");
5458
5459 insta::assert_snapshot!(
5461 env.render_ok(r#"separate(" ", label("error", ""), label("warning", "a"), "b")"#),
5462 @"[38;5;3ma[39m b");
5463
5464 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", ("" ++ ""))"#), @"a");
5466 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", ("" ++ "b"))"#), @"a b");
5467
5468 insta::assert_snapshot!(
5470 env.render_ok(r#"separate(" ", "a", separate("|", "", ""))"#), @"a");
5471 insta::assert_snapshot!(
5472 env.render_ok(r#"separate(" ", "a", separate("|", "b", ""))"#), @"a b");
5473 insta::assert_snapshot!(
5474 env.render_ok(r#"separate(" ", "a", separate("|", "b", "c"))"#), @"a b|c");
5475
5476 insta::assert_snapshot!(
5478 env.render_ok(r#"separate(" ", "a", if(true, ""))"#), @"a");
5479 insta::assert_snapshot!(
5480 env.render_ok(r#"separate(" ", "a", if(true, "", "f"))"#), @"a");
5481 insta::assert_snapshot!(
5482 env.render_ok(r#"separate(" ", "a", if(false, "t"))"#), @"a");
5483 insta::assert_snapshot!(
5484 env.render_ok(r#"separate(" ", "a", if(false, "t", ""))"#), @"a");
5485 insta::assert_snapshot!(
5486 env.render_ok(r#"separate(" ", "a", if(true, "t", "f"))"#), @"a t");
5487
5488 insta::assert_snapshot!(
5490 env.render_ok(r#"separate(" ", hidden, description, empty)"#),
5491 @"false [38;5;2mtrue[39m");
5492
5493 insta::assert_snapshot!(
5495 env.render_ok(r#"separate(hidden, "X", "Y", "Z")"#),
5496 @"XfalseYfalseZ");
5497
5498 insta::assert_snapshot!(env.parse_err(r#"separate(" ", "a", value2="b")"#), @r#"
5500 --> 1:20
5501 |
5502 1 | separate(" ", "a", value2="b")
5503 | ^--------^
5504 |
5505 = Function `separate`: Unexpected keyword arguments
5506 "#);
5507 }
5508
5509 #[test]
5510 fn test_surround_function() {
5511 let mut env = TestTemplateEnv::new();
5512 env.add_keyword("lt", || literal("<".to_owned()));
5513 env.add_keyword("gt", || literal(">".to_owned()));
5514 env.add_keyword("content", || literal("content".to_owned()));
5515 env.add_keyword("empty_content", || literal("".to_owned()));
5516 env.add_color("error", crossterm::style::Color::DarkRed);
5517 env.add_color("paren", crossterm::style::Color::Cyan);
5518
5519 insta::assert_snapshot!(env.render_ok(r#"surround("{", "}", "")"#), @"");
5520 insta::assert_snapshot!(env.render_ok(r#"surround("{", "}", "a")"#), @"{a}");
5521
5522 insta::assert_snapshot!(
5524 env.render_ok(
5525 r#"surround(label("paren", "("), label("paren", ")"), label("error", "a"))"#),
5526 @"[38;5;14m([38;5;1ma[38;5;14m)[39m");
5527
5528 insta::assert_snapshot!(
5530 env.render_ok(r#"surround(lt, gt, content)"#),
5531 @"<content>");
5532 insta::assert_snapshot!(
5533 env.render_ok(r#"surround(lt, gt, empty_content)"#),
5534 @"");
5535
5536 insta::assert_snapshot!(
5538 env.render_ok(r#"surround(lt, gt, if(empty_content, "", "empty"))"#),
5539 @"<empty>");
5540 insta::assert_snapshot!(
5541 env.render_ok(r#"surround(lt, gt, if(empty_content, "not empty", ""))"#),
5542 @"");
5543 }
5544
5545 #[test]
5546 fn test_config_function() {
5547 use jj_lib::config::ConfigLayer;
5548 use jj_lib::config::ConfigSource;
5549
5550 let mut config = StackedConfig::with_defaults();
5551 config
5552 .add_layer(ConfigLayer::parse(ConfigSource::User, "user.name = 'Test User'").unwrap());
5553 config.add_layer(
5554 ConfigLayer::parse(ConfigSource::User, "user.email = 'test@example.com'").unwrap(),
5555 );
5556
5557 let mut env = TestTemplateEnv::with_config(config);
5558
5559 insta::assert_snapshot!(env.render_ok(r#"config("user.name")"#), @"'Test User'");
5561 insta::assert_snapshot!(env.render_ok(r#"config("user.email")"#), @"'test@example.com'");
5562 insta::assert_snapshot!(env.render_ok(r#"config("user")"#), @"{ email = 'test@example.com', name = 'Test User' }");
5563
5564 insta::assert_snapshot!(env.render_ok(r#"config("non.existent")"#), @"");
5566
5567 insta::assert_snapshot!(env.render_ok(r#"if(config("user.name"), "yes", "no")"#), @"yes");
5569 insta::assert_snapshot!(env.render_ok(r#"if(config("non.existent"), "yes", "no")"#), @"no");
5570
5571 env.add_alias("bad_config_name", "'user|name'");
5573 insta::assert_snapshot!(env.parse_err("config(bad_config_name)"), @"
5574 --> 1:8
5575 |
5576 1 | config(bad_config_name)
5577 | ^-------------^
5578 |
5579 = In alias `bad_config_name`
5580 --> 1:1
5581 |
5582 1 | 'user|name'
5583 | ^---------^
5584 |
5585 = Failed to parse config name
5586 TOML parse error at line 1, column 5
5587 |
5588 1 | user|name
5589 | ^
5590 invalid unquoted key, expected letters, numbers, `-`, `_`
5591 ");
5592
5593 env.add_alias("config_key", r#""name""#);
5595 insta::assert_snapshot!(env.render_ok(r#"config("user." ++ "name")"#), @"'Test User'");
5596 insta::assert_snapshot!(env.render_ok(r#"config("us" ++ "er")"#), @"{ email = 'test@example.com', name = 'Test User' }");
5597 insta::assert_snapshot!(env.render_ok(r#"config("user." ++ config_key)"#), @"'Test User'");
5598
5599 insta::assert_snapshot!(env.parse_err(r#"config("user." ++)"#), @r#"
5601 --> 1:18
5602 |
5603 1 | config("user." ++)
5604 | ^---
5605 |
5606 = expected <expression>
5607 "#);
5608 insta::assert_snapshot!(env.parse_err(r#"config("user|" ++ "name")"#), @r#"
5609 --> 1:8
5610 |
5611 1 | config("user|" ++ "name")
5612 | ^---------------^
5613 |
5614 = Failed to parse config name
5615 TOML parse error at line 1, column 5
5616 |
5617 1 | user|name
5618 | ^
5619 invalid unquoted key, expected letters, numbers, `-`, `_`
5620 "#);
5621 insta::assert_snapshot!(env.parse_err(r#"config(invalid)"#), @"
5622 --> 1:8
5623 |
5624 1 | config(invalid)
5625 | ^-----^
5626 |
5627 = Keyword `invalid` doesn't exist
5628 ");
5629
5630 env.add_dynamic_keyword("dyn_config_name", || "user.name".to_owned());
5632 insta::assert_snapshot!(
5633 env.render_ok(r#"config(dyn_config_name)"#), @"'Test User'"
5634 );
5635
5636 env.add_dynamic_keyword("dyn_missing", || "non.existent".to_owned());
5638 insta::assert_snapshot!(env.render_ok(r#"config(dyn_missing)"#), @"");
5639
5640 env.add_dynamic_keyword("dyn_bad_path", || "user|name".to_owned());
5642 insta::assert_snapshot!(env.render_ok(r#"config(dyn_bad_path)"#), @r"
5643 <Error: TOML parse error at line 1, column 5
5644 |
5645 1 | user|name
5646 | ^
5647 invalid unquoted key, expected letters, numbers, `-`, `_`
5648 >
5649 ");
5650
5651 env.add_keyword("bad_string", || new_error_property::<String>("Bad"));
5653 insta::assert_snapshot!(env.render_ok(r#"config(bad_string)"#), @"<Error: Bad>");
5654 }
5655
5656 #[test]
5657 fn test_any_type() {
5658 let mut env = TestTemplateEnv::new();
5659 env.add_keyword("size_hint", || literal((5, None)));
5660 env.add_keyword("size_hint_2", || literal((10, None)));
5661 env.add_keyword("words", || {
5662 literal(vec!["foo".to_owned(), "bar".to_owned()])
5663 });
5664 env.add_color("red", crossterm::style::Color::Red);
5665
5666 insta::assert_snapshot!(env.render_ok(r#"if(true, label("red", "a"), "b")"#), @"[38;5;9ma[39m");
5668 insta::assert_snapshot!(env.render_ok(r#"if(false, label("red", "a"), "b")"#), @"b");
5669 insta::assert_snapshot!(env.render_ok(r#"json(if(true, size_hint, size_hint_2))"#), @"[5,null]");
5670 insta::assert_snapshot!(env.render_ok(r#"json(if(false, size_hint, size_hint_2))"#), @"[10,null]");
5671
5672 insta::assert_snapshot!(env.parse_err(r#"if(true, label("red", "a"), size_hint)"#), @r#"
5675 --> 1:1
5676 |
5677 1 | if(true, label("red", "a"), size_hint)
5678 | ^------------------------------------^
5679 |
5680 = Expected expression of type `Template`, but actual type is `Any`
5681 "#);
5682 insta::assert_snapshot!(env.parse_err(r#"json(if(true, size_hint, label("red", "a")))"#), @r#"
5683 --> 1:6
5684 |
5685 1 | json(if(true, size_hint, label("red", "a")))
5686 | ^------------------------------------^
5687 |
5688 = Expected expression of type `Serialize`, but actual type is `Any`
5689 "#);
5690
5691 insta::assert_snapshot!(env.parse_err(r#"if(true,words,words).join(", ")"#), @r#"
5693 --> 1:22
5694 |
5695 1 | if(true,words,words).join(", ")
5696 | ^--^
5697 |
5698 = Method `join` doesn't exist for type `Any`
5699 "#);
5700 }
5701
5702 #[test]
5703 fn test_any_list_type() {
5704 let mut env = TestTemplateEnv::new();
5705 env.add_keyword("words", || {
5706 literal(vec!["foo".to_owned(), "bar".to_owned()])
5707 });
5708 env.add_keyword("size_hint", || literal((10, None)));
5709 env.add_color("red", crossterm::style::Color::Red);
5710
5711 insta::assert_snapshot!(env.render_ok(
5713 r#"words.map(|x| label("red", x))"#),
5714 @"[38;5;9mfoo[39m [38;5;9mbar[39m");
5715 insta::assert_snapshot!(env.render_ok(
5716 r#"words.map(|x| label("red", x)).join(",")"#),
5717 @"[38;5;9mfoo[39m,[38;5;9mbar[39m");
5718 insta::assert_snapshot!(env.render_ok(
5719 r#"json(words.map(|x| size_hint))"#),
5720 @"[[10,null],[10,null]]");
5721
5722 insta::assert_snapshot!(env.parse_err(r#"words.map(|x| size_hint)"#), @r#"
5724 --> 1:1
5725 |
5726 1 | words.map(|x| size_hint)
5727 | ^----------------------^
5728 |
5729 = Expected expression of type `Template`, but actual type is `AnyList`
5730 "#);
5731 insta::assert_snapshot!(env.parse_err(r#"words.map(|x| size_hint).join(",")"#), @r#"
5732 --> 1:26
5733 |
5734 1 | words.map(|x| size_hint).join(",")
5735 | ^--^
5736 |
5737 = Expected expression of type `Template`, but actual type is `AnyList`
5738 "#);
5739 insta::assert_snapshot!(env.parse_err(r#"json(words.map(|x| label("red", x)))"#), @r#"
5740 --> 1:6
5741 |
5742 1 | json(words.map(|x| label("red", x)))
5743 | ^----------------------------^
5744 |
5745 = Expected expression of type `Serialize`, but actual type is `AnyList`
5746 "#);
5747 }
5748}