1use std::cmp::Ordering;
16use std::collections::HashMap;
17use std::io;
18use std::iter;
19
20use itertools::Itertools as _;
21use jj_lib::backend::Signature;
22use jj_lib::backend::Timestamp;
23use jj_lib::config::ConfigGetResultExt as _;
24use jj_lib::config::ConfigNamePathBuf;
25use jj_lib::config::ConfigValue;
26use jj_lib::content_hash::blake2b_hash;
27use jj_lib::hex_util;
28use jj_lib::op_store::TimestampRange;
29use jj_lib::settings::UserSettings;
30use jj_lib::time_util::DatePattern;
31use serde::Deserialize;
32use serde::de::IntoDeserializer as _;
33
34use crate::config;
35use crate::formatter::FormatRecorder;
36use crate::formatter::Formatter;
37use crate::template_parser;
38use crate::template_parser::BinaryOp;
39use crate::template_parser::ExpressionKind;
40use crate::template_parser::ExpressionNode;
41use crate::template_parser::FunctionCallNode;
42use crate::template_parser::LambdaNode;
43use crate::template_parser::TemplateAliasesMap;
44use crate::template_parser::TemplateDiagnostics;
45use crate::template_parser::TemplateParseError;
46use crate::template_parser::TemplateParseErrorKind;
47use crate::template_parser::TemplateParseResult;
48use crate::template_parser::UnaryOp;
49use crate::templater::BoxedSerializeProperty;
50use crate::templater::BoxedTemplateProperty;
51use crate::templater::CoalesceTemplate;
52use crate::templater::ConcatTemplate;
53use crate::templater::ConditionalTemplate;
54use crate::templater::Email;
55use crate::templater::HyperlinkTemplate;
56use crate::templater::JoinTemplate;
57use crate::templater::LabelTemplate;
58use crate::templater::ListPropertyTemplate;
59use crate::templater::ListTemplate;
60use crate::templater::Literal;
61use crate::templater::PlainTextFormattedProperty;
62use crate::templater::PropertyPlaceholder;
63use crate::templater::RawEscapeSequenceTemplate;
64use crate::templater::ReformatTemplate;
65use crate::templater::SeparateTemplate;
66use crate::templater::SizeHint;
67use crate::templater::Template;
68use crate::templater::TemplateProperty;
69use crate::templater::TemplatePropertyError;
70use crate::templater::TemplatePropertyExt as _;
71use crate::templater::TemplateRenderer;
72use crate::templater::WrapTemplateProperty;
73use crate::text_util;
74use crate::time_util;
75
76pub trait TemplateLanguage<'a> {
82 type Property: CoreTemplatePropertyVar<'a>;
83
84 fn settings(&self) -> &UserSettings;
85
86 fn build_function(
91 &self,
92 diagnostics: &mut TemplateDiagnostics,
93 build_ctx: &BuildContext<Self::Property>,
94 function: &FunctionCallNode,
95 ) -> TemplateParseResult<Self::Property>;
96
97 fn build_method(
100 &self,
101 diagnostics: &mut TemplateDiagnostics,
102 build_ctx: &BuildContext<Self::Property>,
103 property: Self::Property,
104 function: &FunctionCallNode,
105 ) -> TemplateParseResult<Self::Property>;
106}
107
108macro_rules! impl_property_wrappers {
116 ($kind:path $(=> $var:ident)? { $($body:tt)* }) => {
117 $crate::template_builder::_impl_property_wrappers_many!(
118 [], 'static, $kind $(=> $var)?, { $($body)* });
119 };
120 (<$a:lifetime $(, $p:lifetime)* $(, $q:ident)*>
122 $kind:path $(=> $var:ident)? { $($body:tt)* }) => {
123 $crate::template_builder::_impl_property_wrappers_many!(
124 [$a, $($p,)* $($q,)*], $a, $kind $(=> $var)?, { $($body)* });
125 };
126}
127
128macro_rules! _impl_property_wrappers_many {
129 ($ps:tt, $a:lifetime, $kind:path, { $( $var:ident($ty:ty), )* }) => {
132 $(
133 $crate::template_builder::_impl_property_wrappers_one!(
134 $ps, $a, $kind, $var, $ty, std::convert::identity);
135 )*
136 };
137 ($ps:tt, $a:lifetime, $kind:path => $var:ident, { $( $ignored_var:ident($ty:ty), )* }) => {
140 $(
141 $crate::template_builder::_impl_property_wrappers_one!(
142 $ps, $a, $kind, $var, $ty, $crate::templater::WrapTemplateProperty::wrap_property);
143 )*
144 };
145}
146
147macro_rules! _impl_property_wrappers_one {
148 ([$($p:tt)*], $a:lifetime, $kind:path, $var:ident, $ty:ty, $inner:path) => {
149 impl<$($p)*> $crate::templater::WrapTemplateProperty<$a, $ty> for $kind {
150 fn wrap_property(property: $crate::templater::BoxedTemplateProperty<$a, $ty>) -> Self {
151 Self::$var($inner(property))
152 }
153 }
154 };
155}
156
157pub(crate) use _impl_property_wrappers_many;
158pub(crate) use _impl_property_wrappers_one;
159pub(crate) use impl_property_wrappers;
160
161pub trait CoreTemplatePropertyVar<'a>
163where
164 Self: WrapTemplateProperty<'a, String>,
165 Self: WrapTemplateProperty<'a, Vec<String>>,
166 Self: WrapTemplateProperty<'a, bool>,
167 Self: WrapTemplateProperty<'a, i64>,
168 Self: WrapTemplateProperty<'a, Option<i64>>,
169 Self: WrapTemplateProperty<'a, ConfigValue>,
170 Self: WrapTemplateProperty<'a, Option<ConfigValue>>,
171 Self: WrapTemplateProperty<'a, Signature>,
172 Self: WrapTemplateProperty<'a, Email>,
173 Self: WrapTemplateProperty<'a, SizeHint>,
174 Self: WrapTemplateProperty<'a, Timestamp>,
175 Self: WrapTemplateProperty<'a, TimestampRange>,
176{
177 fn wrap_template(template: Box<dyn Template + 'a>) -> Self;
178 fn wrap_list_template(template: Box<dyn ListTemplate + 'a>) -> Self;
179
180 fn type_name(&self) -> &'static str;
182
183 fn try_into_boolean(self) -> Option<BoxedTemplateProperty<'a, bool>>;
184 fn try_into_integer(self) -> Option<BoxedTemplateProperty<'a, i64>>;
185
186 fn try_into_stringify(self) -> Option<BoxedTemplateProperty<'a, String>>;
188 fn try_into_serialize(self) -> Option<BoxedSerializeProperty<'a>>;
189 fn try_into_template(self) -> Option<Box<dyn Template + 'a>>;
190
191 fn try_into_eq(self, other: Self) -> Option<BoxedTemplateProperty<'a, bool>>;
193
194 fn try_into_cmp(self, other: Self) -> Option<BoxedTemplateProperty<'a, Ordering>>;
196}
197
198pub enum CoreTemplatePropertyKind<'a> {
199 String(BoxedTemplateProperty<'a, String>),
200 StringList(BoxedTemplateProperty<'a, Vec<String>>),
201 Boolean(BoxedTemplateProperty<'a, bool>),
202 Integer(BoxedTemplateProperty<'a, i64>),
203 IntegerOpt(BoxedTemplateProperty<'a, Option<i64>>),
204 ConfigValue(BoxedTemplateProperty<'a, ConfigValue>),
205 ConfigValueOpt(BoxedTemplateProperty<'a, Option<ConfigValue>>),
206 Signature(BoxedTemplateProperty<'a, Signature>),
207 Email(BoxedTemplateProperty<'a, Email>),
208 SizeHint(BoxedTemplateProperty<'a, SizeHint>),
209 Timestamp(BoxedTemplateProperty<'a, Timestamp>),
210 TimestampRange(BoxedTemplateProperty<'a, TimestampRange>),
211
212 Template(Box<dyn Template + 'a>),
223 ListTemplate(Box<dyn ListTemplate + 'a>),
224}
225
226macro_rules! impl_core_property_wrappers {
231 ($($head:tt)+) => {
232 $crate::template_builder::impl_property_wrappers!($($head)+ {
233 String(String),
234 StringList(Vec<String>),
235 Boolean(bool),
236 Integer(i64),
237 IntegerOpt(Option<i64>),
238 ConfigValue(jj_lib::config::ConfigValue),
239 ConfigValueOpt(Option<jj_lib::config::ConfigValue>),
240 Signature(jj_lib::backend::Signature),
241 Email($crate::templater::Email),
242 SizeHint($crate::templater::SizeHint),
243 Timestamp(jj_lib::backend::Timestamp),
244 TimestampRange(jj_lib::op_store::TimestampRange),
245 });
246 };
247}
248
249pub(crate) use impl_core_property_wrappers;
250
251impl_core_property_wrappers!(<'a> CoreTemplatePropertyKind<'a>);
252
253impl<'a> CoreTemplatePropertyVar<'a> for CoreTemplatePropertyKind<'a> {
254 fn wrap_template(template: Box<dyn Template + 'a>) -> Self {
255 Self::Template(template)
256 }
257
258 fn wrap_list_template(template: Box<dyn ListTemplate + 'a>) -> Self {
259 Self::ListTemplate(template)
260 }
261
262 fn type_name(&self) -> &'static str {
263 match self {
264 Self::String(_) => "String",
265 Self::StringList(_) => "List<String>",
266 Self::Boolean(_) => "Boolean",
267 Self::Integer(_) => "Integer",
268 Self::IntegerOpt(_) => "Option<Integer>",
269 Self::ConfigValue(_) => "ConfigValue",
270 Self::ConfigValueOpt(_) => "Option<ConfigValue>",
271 Self::Signature(_) => "Signature",
272 Self::Email(_) => "Email",
273 Self::SizeHint(_) => "SizeHint",
274 Self::Timestamp(_) => "Timestamp",
275 Self::TimestampRange(_) => "TimestampRange",
276 Self::Template(_) => "Template",
277 Self::ListTemplate(_) => "ListTemplate",
278 }
279 }
280
281 fn try_into_boolean(self) -> Option<BoxedTemplateProperty<'a, bool>> {
282 match self {
283 Self::String(property) => Some(property.map(|s| !s.is_empty()).into_dyn()),
284 Self::StringList(property) => Some(property.map(|l| !l.is_empty()).into_dyn()),
285 Self::Boolean(property) => Some(property),
286 Self::Integer(_) => None,
287 Self::IntegerOpt(property) => Some(property.map(|opt| opt.is_some()).into_dyn()),
288 Self::ConfigValue(_) => None,
289 Self::ConfigValueOpt(property) => Some(property.map(|opt| opt.is_some()).into_dyn()),
290 Self::Signature(_) => None,
291 Self::Email(property) => Some(property.map(|e| !e.0.is_empty()).into_dyn()),
292 Self::SizeHint(_) => None,
293 Self::Timestamp(_) => None,
294 Self::TimestampRange(_) => None,
295 Self::Template(_) => None,
299 Self::ListTemplate(_) => None,
300 }
301 }
302
303 fn try_into_integer(self) -> Option<BoxedTemplateProperty<'a, i64>> {
304 match self {
305 Self::Integer(property) => Some(property),
306 Self::IntegerOpt(property) => Some(property.try_unwrap("Integer").into_dyn()),
307 _ => None,
308 }
309 }
310
311 fn try_into_stringify(self) -> Option<BoxedTemplateProperty<'a, String>> {
312 match self {
313 Self::String(property) => Some(property),
314 _ => {
315 let template = self.try_into_template()?;
316 Some(PlainTextFormattedProperty::new(template).into_dyn())
317 }
318 }
319 }
320
321 fn try_into_serialize(self) -> Option<BoxedSerializeProperty<'a>> {
322 match self {
323 Self::String(property) => Some(property.into_serialize()),
324 Self::StringList(property) => Some(property.into_serialize()),
325 Self::Boolean(property) => Some(property.into_serialize()),
326 Self::Integer(property) => Some(property.into_serialize()),
327 Self::IntegerOpt(property) => Some(property.into_serialize()),
328 Self::ConfigValue(property) => {
329 Some(property.map(config::to_serializable_value).into_serialize())
330 }
331 Self::ConfigValueOpt(property) => Some(
332 property
333 .map(|opt| opt.map(config::to_serializable_value))
334 .into_serialize(),
335 ),
336 Self::Signature(property) => Some(property.into_serialize()),
337 Self::Email(property) => Some(property.into_serialize()),
338 Self::SizeHint(property) => Some(property.into_serialize()),
339 Self::Timestamp(property) => Some(property.into_serialize()),
340 Self::TimestampRange(property) => Some(property.into_serialize()),
341 Self::Template(_) => None,
342 Self::ListTemplate(_) => None,
343 }
344 }
345
346 fn try_into_template(self) -> Option<Box<dyn Template + 'a>> {
347 match self {
348 Self::String(property) => Some(property.into_template()),
349 Self::StringList(property) => Some(property.into_template()),
350 Self::Boolean(property) => Some(property.into_template()),
351 Self::Integer(property) => Some(property.into_template()),
352 Self::IntegerOpt(property) => Some(property.into_template()),
353 Self::ConfigValue(property) => Some(property.into_template()),
354 Self::ConfigValueOpt(property) => Some(property.into_template()),
355 Self::Signature(property) => Some(property.into_template()),
356 Self::Email(property) => Some(property.into_template()),
357 Self::SizeHint(_) => None,
358 Self::Timestamp(property) => Some(property.into_template()),
359 Self::TimestampRange(property) => Some(property.into_template()),
360 Self::Template(template) => Some(template),
361 Self::ListTemplate(template) => Some(template),
362 }
363 }
364
365 fn try_into_eq(self, other: Self) -> Option<BoxedTemplateProperty<'a, bool>> {
366 match (self, other) {
367 (Self::String(lhs), Self::String(rhs)) => {
368 Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
369 }
370 (Self::String(lhs), Self::Email(rhs)) => {
371 Some((lhs, rhs).map(|(l, r)| l == r.0).into_dyn())
372 }
373 (Self::Boolean(lhs), Self::Boolean(rhs)) => {
374 Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
375 }
376 (Self::Integer(lhs), Self::Integer(rhs)) => {
377 Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
378 }
379 (Self::Integer(lhs), Self::IntegerOpt(rhs)) => {
380 Some((lhs, rhs).map(|(l, r)| Some(l) == r).into_dyn())
381 }
382 (Self::IntegerOpt(lhs), Self::Integer(rhs)) => {
383 Some((lhs, rhs).map(|(l, r)| l == Some(r)).into_dyn())
384 }
385 (Self::IntegerOpt(lhs), Self::IntegerOpt(rhs)) => {
386 Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
387 }
388 (Self::Email(lhs), Self::Email(rhs)) => {
389 Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
390 }
391 (Self::Email(lhs), Self::String(rhs)) => {
392 Some((lhs, rhs).map(|(l, r)| l.0 == r).into_dyn())
393 }
394 (Self::String(_), _) => None,
395 (Self::StringList(_), _) => None,
396 (Self::Boolean(_), _) => None,
397 (Self::Integer(_), _) => None,
398 (Self::IntegerOpt(_), _) => None,
399 (Self::ConfigValue(_), _) => None,
400 (Self::ConfigValueOpt(_), _) => None,
401 (Self::Signature(_), _) => None,
402 (Self::Email(_), _) => None,
403 (Self::SizeHint(_), _) => None,
404 (Self::Timestamp(_), _) => None,
405 (Self::TimestampRange(_), _) => None,
406 (Self::Template(_), _) => None,
407 (Self::ListTemplate(_), _) => None,
408 }
409 }
410
411 fn try_into_cmp(self, other: Self) -> Option<BoxedTemplateProperty<'a, Ordering>> {
412 match (self, other) {
413 (Self::Integer(lhs), Self::Integer(rhs)) => {
414 Some((lhs, rhs).map(|(l, r)| l.cmp(&r)).into_dyn())
415 }
416 (Self::Integer(lhs), Self::IntegerOpt(rhs)) => {
417 Some((lhs, rhs).map(|(l, r)| Some(l).cmp(&r)).into_dyn())
418 }
419 (Self::IntegerOpt(lhs), Self::Integer(rhs)) => {
420 Some((lhs, rhs).map(|(l, r)| l.cmp(&Some(r))).into_dyn())
421 }
422 (Self::IntegerOpt(lhs), Self::IntegerOpt(rhs)) => {
423 Some((lhs, rhs).map(|(l, r)| l.cmp(&r)).into_dyn())
424 }
425 (Self::String(_), _) => None,
426 (Self::StringList(_), _) => None,
427 (Self::Boolean(_), _) => None,
428 (Self::Integer(_), _) => None,
429 (Self::IntegerOpt(_), _) => None,
430 (Self::ConfigValue(_), _) => None,
431 (Self::ConfigValueOpt(_), _) => None,
432 (Self::Signature(_), _) => None,
433 (Self::Email(_), _) => None,
434 (Self::SizeHint(_), _) => None,
435 (Self::Timestamp(_), _) => None,
436 (Self::TimestampRange(_), _) => None,
437 (Self::Template(_), _) => None,
438 (Self::ListTemplate(_), _) => None,
439 }
440 }
441}
442
443pub type TemplateBuildFunctionFn<'a, L, P> =
450 fn(&L, &mut TemplateDiagnostics, &BuildContext<P>, &FunctionCallNode) -> TemplateParseResult<P>;
451
452type BuildMethodFn<'a, L, T, P> = fn(
453 &L,
454 &mut TemplateDiagnostics,
455 &BuildContext<P>,
456 T,
457 &FunctionCallNode,
458) -> TemplateParseResult<P>;
459
460pub type TemplateBuildMethodFn<'a, L, T, P> = BuildMethodFn<'a, L, BoxedTemplateProperty<'a, T>, P>;
462
463pub type BuildTemplateMethodFn<'a, L, P> = BuildMethodFn<'a, L, Box<dyn Template + 'a>, P>;
465
466pub type BuildListTemplateMethodFn<'a, L, P> = BuildMethodFn<'a, L, Box<dyn ListTemplate + 'a>, P>;
468
469pub type TemplateBuildFunctionFnMap<'a, L, P = <L as TemplateLanguage<'a>>::Property> =
471 HashMap<&'static str, TemplateBuildFunctionFn<'a, L, P>>;
472
473pub type TemplateBuildMethodFnMap<'a, L, T, P = <L as TemplateLanguage<'a>>::Property> =
475 HashMap<&'static str, TemplateBuildMethodFn<'a, L, T, P>>;
476
477pub type BuildTemplateMethodFnMap<'a, L, P = <L as TemplateLanguage<'a>>::Property> =
479 HashMap<&'static str, BuildTemplateMethodFn<'a, L, P>>;
480
481pub type BuildListTemplateMethodFnMap<'a, L, P = <L as TemplateLanguage<'a>>::Property> =
483 HashMap<&'static str, BuildListTemplateMethodFn<'a, L, P>>;
484
485pub struct CoreTemplateBuildFnTable<'a, L: ?Sized, P = <L as TemplateLanguage<'a>>::Property> {
487 pub functions: TemplateBuildFunctionFnMap<'a, L, P>,
488 pub string_methods: TemplateBuildMethodFnMap<'a, L, String, P>,
489 pub string_list_methods: TemplateBuildMethodFnMap<'a, L, Vec<String>, P>,
490 pub boolean_methods: TemplateBuildMethodFnMap<'a, L, bool, P>,
491 pub integer_methods: TemplateBuildMethodFnMap<'a, L, i64, P>,
492 pub config_value_methods: TemplateBuildMethodFnMap<'a, L, ConfigValue, P>,
493 pub email_methods: TemplateBuildMethodFnMap<'a, L, Email, P>,
494 pub signature_methods: TemplateBuildMethodFnMap<'a, L, Signature, P>,
495 pub size_hint_methods: TemplateBuildMethodFnMap<'a, L, SizeHint, P>,
496 pub timestamp_methods: TemplateBuildMethodFnMap<'a, L, Timestamp, P>,
497 pub timestamp_range_methods: TemplateBuildMethodFnMap<'a, L, TimestampRange, P>,
498 pub template_methods: BuildTemplateMethodFnMap<'a, L, P>,
499 pub list_template_methods: BuildListTemplateMethodFnMap<'a, L, P>,
500}
501
502pub fn merge_fn_map<'s, F>(base: &mut HashMap<&'s str, F>, extension: HashMap<&'s str, F>) {
503 for (name, function) in extension {
504 if base.insert(name, function).is_some() {
505 panic!("Conflicting template definitions for '{name}' function");
506 }
507 }
508}
509
510impl<L: ?Sized, P> CoreTemplateBuildFnTable<'_, L, P> {
511 pub fn empty() -> Self {
512 Self {
513 functions: HashMap::new(),
514 string_methods: HashMap::new(),
515 string_list_methods: HashMap::new(),
516 boolean_methods: HashMap::new(),
517 integer_methods: HashMap::new(),
518 config_value_methods: HashMap::new(),
519 signature_methods: HashMap::new(),
520 email_methods: HashMap::new(),
521 size_hint_methods: HashMap::new(),
522 timestamp_methods: HashMap::new(),
523 timestamp_range_methods: HashMap::new(),
524 template_methods: HashMap::new(),
525 list_template_methods: HashMap::new(),
526 }
527 }
528
529 pub fn merge(&mut self, other: Self) {
530 let Self {
531 functions,
532 string_methods,
533 string_list_methods,
534 boolean_methods,
535 integer_methods,
536 config_value_methods,
537 signature_methods,
538 email_methods,
539 size_hint_methods,
540 timestamp_methods,
541 timestamp_range_methods,
542 template_methods,
543 list_template_methods,
544 } = other;
545
546 merge_fn_map(&mut self.functions, functions);
547 merge_fn_map(&mut self.string_methods, string_methods);
548 merge_fn_map(&mut self.string_list_methods, string_list_methods);
549 merge_fn_map(&mut self.boolean_methods, boolean_methods);
550 merge_fn_map(&mut self.integer_methods, integer_methods);
551 merge_fn_map(&mut self.config_value_methods, config_value_methods);
552 merge_fn_map(&mut self.signature_methods, signature_methods);
553 merge_fn_map(&mut self.email_methods, email_methods);
554 merge_fn_map(&mut self.size_hint_methods, size_hint_methods);
555 merge_fn_map(&mut self.timestamp_methods, timestamp_methods);
556 merge_fn_map(&mut self.timestamp_range_methods, timestamp_range_methods);
557 merge_fn_map(&mut self.template_methods, template_methods);
558 merge_fn_map(&mut self.list_template_methods, list_template_methods);
559 }
560}
561
562impl<'a, L> CoreTemplateBuildFnTable<'a, L, L::Property>
563where
564 L: TemplateLanguage<'a> + ?Sized,
565{
566 pub fn builtin() -> Self {
568 Self {
569 functions: builtin_functions(),
570 string_methods: builtin_string_methods(),
571 string_list_methods: builtin_formattable_list_methods(),
572 boolean_methods: HashMap::new(),
573 integer_methods: HashMap::new(),
574 config_value_methods: builtin_config_value_methods(),
575 signature_methods: builtin_signature_methods(),
576 email_methods: builtin_email_methods(),
577 size_hint_methods: builtin_size_hint_methods(),
578 timestamp_methods: builtin_timestamp_methods(),
579 timestamp_range_methods: builtin_timestamp_range_methods(),
580 template_methods: HashMap::new(),
581 list_template_methods: builtin_list_template_methods(),
582 }
583 }
584
585 pub fn build_function(
587 &self,
588 language: &L,
589 diagnostics: &mut TemplateDiagnostics,
590 build_ctx: &BuildContext<L::Property>,
591 function: &FunctionCallNode,
592 ) -> TemplateParseResult<L::Property> {
593 let table = &self.functions;
594 let build = template_parser::lookup_function(table, function)?;
595 build(language, diagnostics, build_ctx, function)
596 }
597
598 pub fn build_method(
601 &self,
602 language: &L,
603 diagnostics: &mut TemplateDiagnostics,
604 build_ctx: &BuildContext<L::Property>,
605 property: CoreTemplatePropertyKind<'a>,
606 function: &FunctionCallNode,
607 ) -> TemplateParseResult<L::Property> {
608 let type_name = property.type_name();
609 match property {
610 CoreTemplatePropertyKind::String(property) => {
611 let table = &self.string_methods;
612 let build = template_parser::lookup_method(type_name, table, function)?;
613 build(language, diagnostics, build_ctx, property, function)
614 }
615 CoreTemplatePropertyKind::StringList(property) => {
616 let table = &self.string_list_methods;
617 let build = template_parser::lookup_method(type_name, table, function)?;
618 build(language, diagnostics, build_ctx, property, function)
619 }
620 CoreTemplatePropertyKind::Boolean(property) => {
621 let table = &self.boolean_methods;
622 let build = template_parser::lookup_method(type_name, table, function)?;
623 build(language, diagnostics, build_ctx, property, function)
624 }
625 CoreTemplatePropertyKind::Integer(property) => {
626 let table = &self.integer_methods;
627 let build = template_parser::lookup_method(type_name, table, function)?;
628 build(language, diagnostics, build_ctx, property, function)
629 }
630 CoreTemplatePropertyKind::IntegerOpt(property) => {
631 let type_name = "Integer";
632 let table = &self.integer_methods;
633 let build = template_parser::lookup_method(type_name, table, function)?;
634 let inner_property = property.try_unwrap(type_name).into_dyn();
635 build(language, diagnostics, build_ctx, inner_property, function)
636 }
637 CoreTemplatePropertyKind::ConfigValue(property) => {
638 let table = &self.config_value_methods;
639 let build = template_parser::lookup_method(type_name, table, function)?;
640 build(language, diagnostics, build_ctx, property, function)
641 }
642 CoreTemplatePropertyKind::ConfigValueOpt(property) => {
643 let type_name = "ConfigValue";
644 let table = &self.config_value_methods;
645 let build = template_parser::lookup_method(type_name, table, function)?;
646 let inner_property = property.try_unwrap(type_name).into_dyn();
647 build(language, diagnostics, build_ctx, inner_property, function)
648 }
649 CoreTemplatePropertyKind::Signature(property) => {
650 let table = &self.signature_methods;
651 let build = template_parser::lookup_method(type_name, table, function)?;
652 build(language, diagnostics, build_ctx, property, function)
653 }
654 CoreTemplatePropertyKind::Email(property) => {
655 let table = &self.email_methods;
656 let build = template_parser::lookup_method(type_name, table, function)?;
657 build(language, diagnostics, build_ctx, property, function)
658 }
659 CoreTemplatePropertyKind::SizeHint(property) => {
660 let table = &self.size_hint_methods;
661 let build = template_parser::lookup_method(type_name, table, function)?;
662 build(language, diagnostics, build_ctx, property, function)
663 }
664 CoreTemplatePropertyKind::Timestamp(property) => {
665 let table = &self.timestamp_methods;
666 let build = template_parser::lookup_method(type_name, table, function)?;
667 build(language, diagnostics, build_ctx, property, function)
668 }
669 CoreTemplatePropertyKind::TimestampRange(property) => {
670 let table = &self.timestamp_range_methods;
671 let build = template_parser::lookup_method(type_name, table, function)?;
672 build(language, diagnostics, build_ctx, property, function)
673 }
674 CoreTemplatePropertyKind::Template(template) => {
675 let table = &self.template_methods;
676 let build = template_parser::lookup_method(type_name, table, function)?;
677 build(language, diagnostics, build_ctx, template, function)
678 }
679 CoreTemplatePropertyKind::ListTemplate(template) => {
680 let table = &self.list_template_methods;
681 let build = template_parser::lookup_method(type_name, table, function)?;
682 build(language, diagnostics, build_ctx, template, function)
683 }
684 }
685 }
686}
687
688pub struct Expression<P> {
690 property: P,
691 labels: Vec<String>,
692}
693
694impl<P> Expression<P> {
695 fn unlabeled(property: P) -> Self {
696 let labels = vec![];
697 Self { property, labels }
698 }
699
700 fn with_label(property: P, label: impl Into<String>) -> Self {
701 let labels = vec![label.into()];
702 Self { property, labels }
703 }
704}
705
706impl<'a, P: CoreTemplatePropertyVar<'a>> Expression<P> {
707 pub fn type_name(&self) -> &'static str {
708 self.property.type_name()
709 }
710
711 pub fn try_into_boolean(self) -> Option<BoxedTemplateProperty<'a, bool>> {
712 self.property.try_into_boolean()
713 }
714
715 pub fn try_into_integer(self) -> Option<BoxedTemplateProperty<'a, i64>> {
716 self.property.try_into_integer()
717 }
718
719 pub fn try_into_stringify(self) -> Option<BoxedTemplateProperty<'a, String>> {
720 self.property.try_into_stringify()
721 }
722
723 pub fn try_into_serialize(self) -> Option<BoxedSerializeProperty<'a>> {
724 self.property.try_into_serialize()
725 }
726
727 pub fn try_into_template(self) -> Option<Box<dyn Template + 'a>> {
728 let template = self.property.try_into_template()?;
729 if self.labels.is_empty() {
730 Some(template)
731 } else {
732 Some(Box::new(LabelTemplate::new(template, Literal(self.labels))))
733 }
734 }
735
736 pub fn try_into_eq(self, other: Self) -> Option<BoxedTemplateProperty<'a, bool>> {
737 self.property.try_into_eq(other.property)
738 }
739
740 pub fn try_into_cmp(self, other: Self) -> Option<BoxedTemplateProperty<'a, Ordering>> {
741 self.property.try_into_cmp(other.property)
742 }
743}
744
745pub struct BuildContext<'i, P> {
747 local_variables: HashMap<&'i str, &'i dyn Fn() -> P>,
749 self_variable: &'i dyn Fn() -> P,
754}
755
756fn build_keyword<'a, L: TemplateLanguage<'a> + ?Sized>(
757 language: &L,
758 diagnostics: &mut TemplateDiagnostics,
759 build_ctx: &BuildContext<L::Property>,
760 name: &str,
761 name_span: pest::Span<'_>,
762) -> TemplateParseResult<L::Property> {
763 let self_property = (build_ctx.self_variable)();
765 let function = FunctionCallNode {
766 name,
767 name_span,
768 args: vec![],
769 keyword_args: vec![],
770 args_span: name_span.end_pos().span(&name_span.end_pos()),
771 };
772 language
773 .build_method(diagnostics, build_ctx, self_property, &function)
774 .map_err(|err| match err.kind() {
775 TemplateParseErrorKind::NoSuchMethod { candidates, .. } => {
776 let kind = TemplateParseErrorKind::NoSuchKeyword {
777 name: name.to_owned(),
778 candidates: candidates.clone(),
780 };
781 TemplateParseError::with_span(kind, name_span)
782 }
783 TemplateParseErrorKind::InvalidArguments { .. } => {
786 let kind = TemplateParseErrorKind::NoSuchKeyword {
787 name: name.to_owned(),
788 candidates: vec![format!("self.{name}(..)")],
790 };
791 TemplateParseError::with_span(kind, name_span)
792 }
793 _ => err,
795 })
796}
797
798fn build_unary_operation<'a, L: TemplateLanguage<'a> + ?Sized>(
799 language: &L,
800 diagnostics: &mut TemplateDiagnostics,
801 build_ctx: &BuildContext<L::Property>,
802 op: UnaryOp,
803 arg_node: &ExpressionNode,
804) -> TemplateParseResult<L::Property> {
805 match op {
806 UnaryOp::LogicalNot => {
807 let arg = expect_boolean_expression(language, diagnostics, build_ctx, arg_node)?;
808 Ok(arg.map(|v| !v).into_dyn_wrapped())
809 }
810 UnaryOp::Negate => {
811 let arg = expect_integer_expression(language, diagnostics, build_ctx, arg_node)?;
812 let out = arg.and_then(|v| {
813 v.checked_neg()
814 .ok_or_else(|| TemplatePropertyError("Attempt to negate with overflow".into()))
815 });
816 Ok(out.into_dyn_wrapped())
817 }
818 }
819}
820
821fn build_binary_operation<'a, L: TemplateLanguage<'a> + ?Sized>(
822 language: &L,
823 diagnostics: &mut TemplateDiagnostics,
824 build_ctx: &BuildContext<L::Property>,
825 op: BinaryOp,
826 lhs_node: &ExpressionNode,
827 rhs_node: &ExpressionNode,
828 span: pest::Span<'_>,
829) -> TemplateParseResult<L::Property> {
830 match op {
831 BinaryOp::LogicalOr => {
832 let lhs = expect_boolean_expression(language, diagnostics, build_ctx, lhs_node)?;
833 let rhs = expect_boolean_expression(language, diagnostics, build_ctx, rhs_node)?;
834 let out = lhs.and_then(move |l| Ok(l || rhs.extract()?));
835 Ok(out.into_dyn_wrapped())
836 }
837 BinaryOp::LogicalAnd => {
838 let lhs = expect_boolean_expression(language, diagnostics, build_ctx, lhs_node)?;
839 let rhs = expect_boolean_expression(language, diagnostics, build_ctx, rhs_node)?;
840 let out = lhs.and_then(move |l| Ok(l && rhs.extract()?));
841 Ok(out.into_dyn_wrapped())
842 }
843 BinaryOp::Eq | BinaryOp::Ne => {
844 let lhs = build_expression(language, diagnostics, build_ctx, lhs_node)?;
845 let rhs = build_expression(language, diagnostics, build_ctx, rhs_node)?;
846 let lty = lhs.type_name();
847 let rty = rhs.type_name();
848 let eq = lhs.try_into_eq(rhs).ok_or_else(|| {
849 let message = format!("Cannot compare expressions of type `{lty}` and `{rty}`");
850 TemplateParseError::expression(message, span)
851 })?;
852 let out = match op {
853 BinaryOp::Eq => eq.into_dyn(),
854 BinaryOp::Ne => eq.map(|eq| !eq).into_dyn(),
855 _ => unreachable!(),
856 };
857 Ok(L::Property::wrap_property(out))
858 }
859 BinaryOp::Ge | BinaryOp::Gt | BinaryOp::Le | BinaryOp::Lt => {
860 let lhs = build_expression(language, diagnostics, build_ctx, lhs_node)?;
861 let rhs = build_expression(language, diagnostics, build_ctx, rhs_node)?;
862 let lty = lhs.type_name();
863 let rty = rhs.type_name();
864 let cmp = lhs.try_into_cmp(rhs).ok_or_else(|| {
865 let message = format!("Cannot compare expressions of type `{lty}` and `{rty}`");
866 TemplateParseError::expression(message, span)
867 })?;
868 let out = match op {
869 BinaryOp::Ge => cmp.map(|ordering| ordering.is_ge()).into_dyn(),
870 BinaryOp::Gt => cmp.map(|ordering| ordering.is_gt()).into_dyn(),
871 BinaryOp::Le => cmp.map(|ordering| ordering.is_le()).into_dyn(),
872 BinaryOp::Lt => cmp.map(|ordering| ordering.is_lt()).into_dyn(),
873 _ => unreachable!(),
874 };
875 Ok(L::Property::wrap_property(out))
876 }
877 BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div | BinaryOp::Rem => {
878 let lhs = expect_integer_expression(language, diagnostics, build_ctx, lhs_node)?;
879 let rhs = expect_integer_expression(language, diagnostics, build_ctx, rhs_node)?;
880 let build = |op: fn(i64, i64) -> Option<i64>, msg: fn(i64) -> &'static str| {
881 (lhs, rhs).and_then(move |(l, r)| {
882 op(l, r).ok_or_else(|| TemplatePropertyError(msg(r).into()))
883 })
884 };
885 let out = match op {
886 BinaryOp::Add => build(i64::checked_add, |_| "Attempt to add with overflow"),
887 BinaryOp::Sub => build(i64::checked_sub, |_| "Attempt to subtract with overflow"),
888 BinaryOp::Mul => build(i64::checked_mul, |_| "Attempt to multiply with overflow"),
889 BinaryOp::Div => build(i64::checked_div, |r| {
890 if r == 0 {
891 "Attempt to divide by zero"
892 } else {
893 "Attempt to divide with overflow"
894 }
895 }),
896 BinaryOp::Rem => build(i64::checked_rem, |r| {
897 if r == 0 {
898 "Attempt to divide by zero"
899 } else {
900 "Attempt to divide with overflow"
901 }
902 }),
903 _ => unreachable!(),
904 };
905 Ok(out.into_dyn_wrapped())
906 }
907 }
908}
909
910fn builtin_string_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
911-> TemplateBuildMethodFnMap<'a, L, String> {
912 let mut map = TemplateBuildMethodFnMap::<L, String>::new();
915 map.insert(
916 "len",
917 |_language, _diagnostics, _build_ctx, self_property, function| {
918 function.expect_no_arguments()?;
919 let out_property = self_property.and_then(|s| Ok(i64::try_from(s.len())?));
920 Ok(out_property.into_dyn_wrapped())
921 },
922 );
923 map.insert(
924 "contains",
925 |language, diagnostics, build_ctx, self_property, function| {
926 let [needle_node] = function.expect_exact_arguments()?;
927 let needle_property =
929 expect_stringify_expression(language, diagnostics, build_ctx, needle_node)?;
930 let out_property = (self_property, needle_property)
931 .map(|(haystack, needle)| haystack.contains(&needle));
932 Ok(out_property.into_dyn_wrapped())
933 },
934 );
935 map.insert(
936 "match",
937 |_language, _diagnostics, _build_ctx, self_property, function| {
938 let [needle_node] = function.expect_exact_arguments()?;
939 let needle = template_parser::expect_string_pattern(needle_node)?;
940 let regex = needle.to_regex();
941
942 let out_property = self_property.and_then(move |haystack| {
943 if let Some(m) = regex.find(haystack.as_bytes()) {
944 Ok(str::from_utf8(m.as_bytes())?.to_owned())
945 } else {
946 Ok(String::new())
949 }
950 });
951 Ok(out_property.into_dyn_wrapped())
952 },
953 );
954 map.insert(
955 "starts_with",
956 |language, diagnostics, build_ctx, self_property, function| {
957 let [needle_node] = function.expect_exact_arguments()?;
958 let needle_property =
959 expect_stringify_expression(language, diagnostics, build_ctx, needle_node)?;
960 let out_property = (self_property, needle_property)
961 .map(|(haystack, needle)| haystack.starts_with(&needle));
962 Ok(out_property.into_dyn_wrapped())
963 },
964 );
965 map.insert(
966 "ends_with",
967 |language, diagnostics, build_ctx, self_property, function| {
968 let [needle_node] = function.expect_exact_arguments()?;
969 let needle_property =
970 expect_stringify_expression(language, diagnostics, build_ctx, needle_node)?;
971 let out_property = (self_property, needle_property)
972 .map(|(haystack, needle)| haystack.ends_with(&needle));
973 Ok(out_property.into_dyn_wrapped())
974 },
975 );
976 map.insert(
977 "remove_prefix",
978 |language, diagnostics, build_ctx, self_property, function| {
979 let [needle_node] = function.expect_exact_arguments()?;
980 let needle_property =
981 expect_stringify_expression(language, diagnostics, build_ctx, needle_node)?;
982 let out_property = (self_property, needle_property).map(|(haystack, needle)| {
983 haystack
984 .strip_prefix(&needle)
985 .map(ToOwned::to_owned)
986 .unwrap_or(haystack)
987 });
988 Ok(out_property.into_dyn_wrapped())
989 },
990 );
991 map.insert(
992 "remove_suffix",
993 |language, diagnostics, build_ctx, self_property, function| {
994 let [needle_node] = function.expect_exact_arguments()?;
995 let needle_property =
996 expect_stringify_expression(language, diagnostics, build_ctx, needle_node)?;
997 let out_property = (self_property, needle_property).map(|(haystack, needle)| {
998 haystack
999 .strip_suffix(&needle)
1000 .map(ToOwned::to_owned)
1001 .unwrap_or(haystack)
1002 });
1003 Ok(out_property.into_dyn_wrapped())
1004 },
1005 );
1006 map.insert(
1007 "trim",
1008 |_language, _diagnostics, _build_ctx, self_property, function| {
1009 function.expect_no_arguments()?;
1010 let out_property = self_property.map(|s| s.trim().to_owned());
1011 Ok(out_property.into_dyn_wrapped())
1012 },
1013 );
1014 map.insert(
1015 "trim_start",
1016 |_language, _diagnostics, _build_ctx, self_property, function| {
1017 function.expect_no_arguments()?;
1018 let out_property = self_property.map(|s| s.trim_start().to_owned());
1019 Ok(out_property.into_dyn_wrapped())
1020 },
1021 );
1022 map.insert(
1023 "trim_end",
1024 |_language, _diagnostics, _build_ctx, self_property, function| {
1025 function.expect_no_arguments()?;
1026 let out_property = self_property.map(|s| s.trim_end().to_owned());
1027 Ok(out_property.into_dyn_wrapped())
1028 },
1029 );
1030 map.insert(
1031 "substr",
1032 |language, diagnostics, build_ctx, self_property, function| {
1033 let [start_idx, end_idx] = function.expect_exact_arguments()?;
1034 let start_idx_property =
1035 expect_isize_expression(language, diagnostics, build_ctx, start_idx)?;
1036 let end_idx_property =
1037 expect_isize_expression(language, diagnostics, build_ctx, end_idx)?;
1038 let out_property = (self_property, start_idx_property, end_idx_property).map(
1039 |(s, start_idx, end_idx)| {
1040 let start_idx = string_index_to_char_boundary(&s, start_idx);
1041 let end_idx = string_index_to_char_boundary(&s, end_idx);
1042 s.get(start_idx..end_idx).unwrap_or_default().to_owned()
1043 },
1044 );
1045 Ok(out_property.into_dyn_wrapped())
1046 },
1047 );
1048 map.insert(
1049 "first_line",
1050 |_language, _diagnostics, _build_ctx, self_property, function| {
1051 function.expect_no_arguments()?;
1052 let out_property =
1053 self_property.map(|s| s.lines().next().unwrap_or_default().to_string());
1054 Ok(out_property.into_dyn_wrapped())
1055 },
1056 );
1057 map.insert(
1058 "lines",
1059 |_language, _diagnostics, _build_ctx, self_property, function| {
1060 function.expect_no_arguments()?;
1061 let out_property = self_property.map(|s| s.lines().map(|l| l.to_owned()).collect_vec());
1062 Ok(out_property.into_dyn_wrapped())
1063 },
1064 );
1065 map.insert(
1066 "split",
1067 |language, diagnostics, build_ctx, self_property, function| {
1068 let ([separator_node], [limit_node]) = function.expect_arguments()?;
1069 let pattern = template_parser::expect_string_pattern(separator_node)?;
1070 let regex = pattern.to_regex();
1071
1072 if let Some(limit_node) = limit_node {
1073 let limit_property =
1074 expect_usize_expression(language, diagnostics, build_ctx, limit_node)?;
1075 let out_property =
1076 (self_property, limit_property).and_then(move |(haystack, limit)| {
1077 let haystack_bytes = haystack.as_bytes();
1078 let parts: Vec<_> = regex
1079 .splitn(haystack_bytes, limit)
1080 .map(|part| str::from_utf8(part).map(|s| s.to_owned()))
1081 .try_collect()?;
1082 Ok(parts)
1083 });
1084 Ok(out_property.into_dyn_wrapped())
1085 } else {
1086 let out_property = self_property.and_then(move |haystack| {
1087 let haystack_bytes = haystack.as_bytes();
1088 let parts: Vec<_> = regex
1089 .split(haystack_bytes)
1090 .map(|part| str::from_utf8(part).map(|s| s.to_owned()))
1091 .try_collect()?;
1092 Ok(parts)
1093 });
1094 Ok(out_property.into_dyn_wrapped())
1095 }
1096 },
1097 );
1098 map.insert(
1099 "upper",
1100 |_language, _diagnostics, _build_ctx, self_property, function| {
1101 function.expect_no_arguments()?;
1102 let out_property = self_property.map(|s| s.to_uppercase());
1103 Ok(out_property.into_dyn_wrapped())
1104 },
1105 );
1106 map.insert(
1107 "lower",
1108 |_language, _diagnostics, _build_ctx, self_property, function| {
1109 function.expect_no_arguments()?;
1110 let out_property = self_property.map(|s| s.to_lowercase());
1111 Ok(out_property.into_dyn_wrapped())
1112 },
1113 );
1114 map.insert(
1115 "escape_json",
1116 |_language, _diagnostics, _build_ctx, self_property, function| {
1117 function.expect_no_arguments()?;
1118 let out_property = self_property.map(|s| serde_json::to_string(&s).unwrap());
1119 Ok(out_property.into_dyn_wrapped())
1120 },
1121 );
1122 map.insert(
1123 "replace",
1124 |language, diagnostics, build_ctx, self_property, function| {
1125 let ([pattern_node, replacement_node], [limit_node]) = function.expect_arguments()?;
1126 let pattern = template_parser::expect_string_pattern(pattern_node)?;
1127 let replacement_property =
1128 expect_stringify_expression(language, diagnostics, build_ctx, replacement_node)?;
1129
1130 let regex = pattern.to_regex();
1131
1132 if let Some(limit_node) = limit_node {
1133 let limit_property =
1134 expect_usize_expression(language, diagnostics, build_ctx, limit_node)?;
1135 let out_property = (self_property, replacement_property, limit_property).and_then(
1136 move |(haystack, replacement, limit)| {
1137 if limit == 0 {
1138 Ok(haystack)
1142 } else {
1143 let haystack_bytes = haystack.as_bytes();
1144 let replace_bytes = replacement.as_bytes();
1145 let result = regex.replacen(haystack_bytes, limit, replace_bytes);
1146 Ok(str::from_utf8(&result)?.to_owned())
1147 }
1148 },
1149 );
1150 Ok(out_property.into_dyn_wrapped())
1151 } else {
1152 let out_property = (self_property, replacement_property).and_then(
1153 move |(haystack, replacement)| {
1154 let haystack_bytes = haystack.as_bytes();
1155 let replace_bytes = replacement.as_bytes();
1156 let result = regex.replace_all(haystack_bytes, replace_bytes);
1157 Ok(str::from_utf8(&result)?.to_owned())
1158 },
1159 );
1160 Ok(out_property.into_dyn_wrapped())
1161 }
1162 },
1163 );
1164 map
1165}
1166
1167fn string_index_to_char_boundary(s: &str, i: isize) -> usize {
1172 let magnitude = i.unsigned_abs();
1174 if i < 0 {
1175 let p = s.len().saturating_sub(magnitude);
1176 (p..=s.len()).find(|&p| s.is_char_boundary(p)).unwrap()
1177 } else {
1178 let p = magnitude.min(s.len());
1179 (0..=p).rev().find(|&p| s.is_char_boundary(p)).unwrap()
1180 }
1181}
1182
1183fn builtin_config_value_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1184-> TemplateBuildMethodFnMap<'a, L, ConfigValue> {
1185 fn extract<'de, T: Deserialize<'de>>(value: ConfigValue) -> Result<T, TemplatePropertyError> {
1186 T::deserialize(value.into_deserializer())
1187 .map_err(|err| TemplatePropertyError(err.message().into()))
1189 }
1190
1191 let mut map = TemplateBuildMethodFnMap::<L, ConfigValue>::new();
1194 map.insert(
1199 "as_boolean",
1200 |_language, _diagnostics, _build_ctx, self_property, function| {
1201 function.expect_no_arguments()?;
1202 let out_property = self_property.and_then(extract::<bool>);
1203 Ok(out_property.into_dyn_wrapped())
1204 },
1205 );
1206 map.insert(
1207 "as_integer",
1208 |_language, _diagnostics, _build_ctx, self_property, function| {
1209 function.expect_no_arguments()?;
1210 let out_property = self_property.and_then(extract::<i64>);
1211 Ok(out_property.into_dyn_wrapped())
1212 },
1213 );
1214 map.insert(
1215 "as_string",
1216 |_language, _diagnostics, _build_ctx, self_property, function| {
1217 function.expect_no_arguments()?;
1218 let out_property = self_property.and_then(extract::<String>);
1219 Ok(out_property.into_dyn_wrapped())
1220 },
1221 );
1222 map.insert(
1223 "as_string_list",
1224 |_language, _diagnostics, _build_ctx, self_property, function| {
1225 function.expect_no_arguments()?;
1226 let out_property = self_property.and_then(extract::<Vec<String>>);
1227 Ok(out_property.into_dyn_wrapped())
1228 },
1229 );
1230 map
1233}
1234
1235fn builtin_signature_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1236-> TemplateBuildMethodFnMap<'a, L, Signature> {
1237 let mut map = TemplateBuildMethodFnMap::<L, Signature>::new();
1240 map.insert(
1241 "name",
1242 |_language, _diagnostics, _build_ctx, self_property, function| {
1243 function.expect_no_arguments()?;
1244 let out_property = self_property.map(|signature| signature.name);
1245 Ok(out_property.into_dyn_wrapped())
1246 },
1247 );
1248 map.insert(
1249 "email",
1250 |_language, _diagnostics, _build_ctx, self_property, function| {
1251 function.expect_no_arguments()?;
1252 let out_property = self_property.map(|signature| Email(signature.email));
1253 Ok(out_property.into_dyn_wrapped())
1254 },
1255 );
1256 map.insert(
1257 "timestamp",
1258 |_language, _diagnostics, _build_ctx, self_property, function| {
1259 function.expect_no_arguments()?;
1260 let out_property = self_property.map(|signature| signature.timestamp);
1261 Ok(out_property.into_dyn_wrapped())
1262 },
1263 );
1264 map
1265}
1266
1267fn builtin_email_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1268-> TemplateBuildMethodFnMap<'a, L, Email> {
1269 let mut map = TemplateBuildMethodFnMap::<L, Email>::new();
1272 map.insert(
1273 "local",
1274 |_language, _diagnostics, _build_ctx, self_property, function| {
1275 function.expect_no_arguments()?;
1276 let out_property = self_property.map(|email| {
1277 let (local, _) = text_util::split_email(&email.0);
1278 local.to_owned()
1279 });
1280 Ok(out_property.into_dyn_wrapped())
1281 },
1282 );
1283 map.insert(
1284 "domain",
1285 |_language, _diagnostics, _build_ctx, self_property, function| {
1286 function.expect_no_arguments()?;
1287 let out_property = self_property.map(|email| {
1288 let (_, domain) = text_util::split_email(&email.0);
1289 domain.unwrap_or_default().to_owned()
1290 });
1291 Ok(out_property.into_dyn_wrapped())
1292 },
1293 );
1294 map
1295}
1296
1297fn builtin_size_hint_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1298-> TemplateBuildMethodFnMap<'a, L, SizeHint> {
1299 let mut map = TemplateBuildMethodFnMap::<L, SizeHint>::new();
1302 map.insert(
1303 "lower",
1304 |_language, _diagnostics, _build_ctx, self_property, function| {
1305 function.expect_no_arguments()?;
1306 let out_property = self_property.and_then(|(lower, _)| Ok(i64::try_from(lower)?));
1307 Ok(out_property.into_dyn_wrapped())
1308 },
1309 );
1310 map.insert(
1311 "upper",
1312 |_language, _diagnostics, _build_ctx, self_property, function| {
1313 function.expect_no_arguments()?;
1314 let out_property =
1315 self_property.and_then(|(_, upper)| Ok(upper.map(i64::try_from).transpose()?));
1316 Ok(out_property.into_dyn_wrapped())
1317 },
1318 );
1319 map.insert(
1320 "exact",
1321 |_language, _diagnostics, _build_ctx, self_property, function| {
1322 function.expect_no_arguments()?;
1323 let out_property = self_property.and_then(|(lower, upper)| {
1324 let exact = (Some(lower) == upper).then_some(lower);
1325 Ok(exact.map(i64::try_from).transpose()?)
1326 });
1327 Ok(out_property.into_dyn_wrapped())
1328 },
1329 );
1330 map.insert(
1331 "zero",
1332 |_language, _diagnostics, _build_ctx, self_property, function| {
1333 function.expect_no_arguments()?;
1334 let out_property = self_property.map(|(_, upper)| upper == Some(0));
1335 Ok(out_property.into_dyn_wrapped())
1336 },
1337 );
1338 map
1339}
1340
1341fn builtin_timestamp_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1342-> TemplateBuildMethodFnMap<'a, L, Timestamp> {
1343 let mut map = TemplateBuildMethodFnMap::<L, Timestamp>::new();
1346 map.insert(
1347 "ago",
1348 |_language, _diagnostics, _build_ctx, self_property, function| {
1349 function.expect_no_arguments()?;
1350 let now = Timestamp::now();
1351 let format = timeago::Formatter::new();
1352 let out_property = self_property.and_then(move |timestamp| {
1353 Ok(time_util::format_duration(×tamp, &now, &format)?)
1354 });
1355 Ok(out_property.into_dyn_wrapped())
1356 },
1357 );
1358 map.insert(
1359 "format",
1360 |_language, diagnostics, _build_ctx, self_property, function| {
1361 let [format_node] = function.expect_exact_arguments()?;
1363 let format =
1364 template_parser::catch_aliases(diagnostics, format_node, |_diagnostics, node| {
1365 let format = template_parser::expect_string_literal(node)?;
1366 time_util::FormattingItems::parse(format).ok_or_else(|| {
1367 TemplateParseError::expression("Invalid time format", node.span)
1368 })
1369 })?
1370 .into_owned();
1371 let out_property = self_property.and_then(move |timestamp| {
1372 Ok(time_util::format_absolute_timestamp_with(
1373 ×tamp, &format,
1374 )?)
1375 });
1376 Ok(out_property.into_dyn_wrapped())
1377 },
1378 );
1379 map.insert(
1380 "utc",
1381 |_language, _diagnostics, _build_ctx, self_property, function| {
1382 function.expect_no_arguments()?;
1383 let out_property = self_property.map(|mut timestamp| {
1384 timestamp.tz_offset = 0;
1385 timestamp
1386 });
1387 Ok(out_property.into_dyn_wrapped())
1388 },
1389 );
1390 map.insert(
1391 "local",
1392 |_language, _diagnostics, _build_ctx, self_property, function| {
1393 function.expect_no_arguments()?;
1394 let tz_offset = std::env::var("JJ_TZ_OFFSET_MINS")
1395 .ok()
1396 .and_then(|tz_string| tz_string.parse::<i32>().ok())
1397 .unwrap_or_else(|| chrono::Local::now().offset().local_minus_utc() / 60);
1398 let out_property = self_property.map(move |mut timestamp| {
1399 timestamp.tz_offset = tz_offset;
1400 timestamp
1401 });
1402 Ok(out_property.into_dyn_wrapped())
1403 },
1404 );
1405 map.insert(
1406 "after",
1407 |_language, diagnostics, _build_ctx, self_property, function| {
1408 let [date_pattern_node] = function.expect_exact_arguments()?;
1409 let now = chrono::Local::now();
1410 let date_pattern = template_parser::catch_aliases(
1411 diagnostics,
1412 date_pattern_node,
1413 |_diagnostics, node| {
1414 let date_pattern = template_parser::expect_string_literal(node)?;
1415 DatePattern::from_str_kind(date_pattern, function.name, now).map_err(|err| {
1416 TemplateParseError::expression("Invalid date pattern", node.span)
1417 .with_source(err)
1418 })
1419 },
1420 )?;
1421 let out_property = self_property.map(move |timestamp| date_pattern.matches(×tamp));
1422 Ok(out_property.into_dyn_wrapped())
1423 },
1424 );
1425 map.insert("before", map["after"]);
1426 map
1427}
1428
1429fn builtin_timestamp_range_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1430-> TemplateBuildMethodFnMap<'a, L, TimestampRange> {
1431 let mut map = TemplateBuildMethodFnMap::<L, TimestampRange>::new();
1434 map.insert(
1435 "start",
1436 |_language, _diagnostics, _build_ctx, self_property, function| {
1437 function.expect_no_arguments()?;
1438 let out_property = self_property.map(|time_range| time_range.start);
1439 Ok(out_property.into_dyn_wrapped())
1440 },
1441 );
1442 map.insert(
1443 "end",
1444 |_language, _diagnostics, _build_ctx, self_property, function| {
1445 function.expect_no_arguments()?;
1446 let out_property = self_property.map(|time_range| time_range.end);
1447 Ok(out_property.into_dyn_wrapped())
1448 },
1449 );
1450 map.insert(
1451 "duration",
1452 |_language, _diagnostics, _build_ctx, self_property, function| {
1453 function.expect_no_arguments()?;
1454 let out_property = self_property.and_then(|time_range| {
1456 let mut f = timeago::Formatter::new();
1457 f.min_unit(timeago::TimeUnit::Microseconds).ago("");
1458 let duration = time_util::format_duration(&time_range.start, &time_range.end, &f)?;
1459 if duration == "now" {
1460 Ok("less than a microsecond".to_owned())
1461 } else {
1462 Ok(duration)
1463 }
1464 });
1465 Ok(out_property.into_dyn_wrapped())
1466 },
1467 );
1468 map
1469}
1470
1471fn builtin_list_template_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1472-> BuildListTemplateMethodFnMap<'a, L> {
1473 let mut map = BuildListTemplateMethodFnMap::<L>::new();
1476 map.insert(
1477 "join",
1478 |language, diagnostics, build_ctx, self_template, function| {
1479 let [separator_node] = function.expect_exact_arguments()?;
1480 let separator =
1481 expect_template_expression(language, diagnostics, build_ctx, separator_node)?;
1482 Ok(L::Property::wrap_template(self_template.join(separator)))
1483 },
1484 );
1485 map
1486}
1487
1488pub fn builtin_formattable_list_methods<'a, L, O>() -> TemplateBuildMethodFnMap<'a, L, Vec<O>>
1490where
1491 L: TemplateLanguage<'a> + ?Sized,
1492 L::Property: WrapTemplateProperty<'a, O> + WrapTemplateProperty<'a, Vec<O>>,
1493 O: Template + Clone + 'a,
1494{
1495 let mut map = builtin_unformattable_list_methods::<L, O>();
1496 map.insert(
1497 "join",
1498 |language, diagnostics, build_ctx, self_property, function| {
1499 let [separator_node] = function.expect_exact_arguments()?;
1500 let separator =
1501 expect_template_expression(language, diagnostics, build_ctx, separator_node)?;
1502 let template =
1503 ListPropertyTemplate::new(self_property, separator, |formatter, item| {
1504 item.format(formatter)
1505 });
1506 Ok(L::Property::wrap_template(Box::new(template)))
1507 },
1508 );
1509 map
1510}
1511
1512pub fn builtin_unformattable_list_methods<'a, L, O>() -> TemplateBuildMethodFnMap<'a, L, Vec<O>>
1514where
1515 L: TemplateLanguage<'a> + ?Sized,
1516 L::Property: WrapTemplateProperty<'a, O> + WrapTemplateProperty<'a, Vec<O>>,
1517 O: Clone + 'a,
1518{
1519 let mut map = TemplateBuildMethodFnMap::<L, Vec<O>>::new();
1522 map.insert(
1523 "len",
1524 |_language, _diagnostics, _build_ctx, self_property, function| {
1525 function.expect_no_arguments()?;
1526 let out_property = self_property.and_then(|items| Ok(i64::try_from(items.len())?));
1527 Ok(out_property.into_dyn_wrapped())
1528 },
1529 );
1530 map.insert(
1531 "filter",
1532 |language, diagnostics, build_ctx, self_property, function| {
1533 let out_property: BoxedTemplateProperty<'a, Vec<O>> =
1534 build_filter_operation(language, diagnostics, build_ctx, self_property, function)?;
1535 Ok(L::Property::wrap_property(out_property))
1536 },
1537 );
1538 map.insert(
1539 "map",
1540 |language, diagnostics, build_ctx, self_property, function| {
1541 let template =
1542 build_map_operation(language, diagnostics, build_ctx, self_property, function)?;
1543 Ok(L::Property::wrap_list_template(template))
1544 },
1545 );
1546 map.insert(
1547 "any",
1548 |language, diagnostics, build_ctx, self_property, function| {
1549 let out_property =
1550 build_any_operation(language, diagnostics, build_ctx, self_property, function)?;
1551 Ok(out_property.into_dyn_wrapped())
1552 },
1553 );
1554 map.insert(
1555 "all",
1556 |language, diagnostics, build_ctx, self_property, function| {
1557 let out_property =
1558 build_all_operation(language, diagnostics, build_ctx, self_property, function)?;
1559 Ok(out_property.into_dyn_wrapped())
1560 },
1561 );
1562 map
1563}
1564
1565fn build_filter_operation<'a, L, O, P, B>(
1567 language: &L,
1568 diagnostics: &mut TemplateDiagnostics,
1569 build_ctx: &BuildContext<L::Property>,
1570 self_property: P,
1571 function: &FunctionCallNode,
1572) -> TemplateParseResult<BoxedTemplateProperty<'a, B>>
1573where
1574 L: TemplateLanguage<'a> + ?Sized,
1575 L::Property: WrapTemplateProperty<'a, O>,
1576 P: TemplateProperty + 'a,
1577 P::Output: IntoIterator<Item = O>,
1578 O: Clone + 'a,
1579 B: FromIterator<O>,
1580{
1581 let [lambda_node] = function.expect_exact_arguments()?;
1582 let item_placeholder = PropertyPlaceholder::new();
1583 let item_predicate =
1584 template_parser::catch_aliases(diagnostics, lambda_node, |diagnostics, node| {
1585 let lambda = template_parser::expect_lambda(node)?;
1586 build_lambda_expression(
1587 build_ctx,
1588 lambda,
1589 &[&|| item_placeholder.clone().into_dyn_wrapped()],
1590 |build_ctx, body| expect_boolean_expression(language, diagnostics, build_ctx, body),
1591 )
1592 })?;
1593 let out_property = self_property.and_then(move |items| {
1594 items
1595 .into_iter()
1596 .filter_map(|item| {
1597 item_placeholder.set(item);
1599 let result = item_predicate.extract();
1600 let item = item_placeholder.take().unwrap();
1601 result.map(|pred| pred.then_some(item)).transpose()
1602 })
1603 .collect()
1604 });
1605 Ok(out_property.into_dyn())
1606}
1607
1608fn build_map_operation<'a, L, O, P>(
1611 language: &L,
1612 diagnostics: &mut TemplateDiagnostics,
1613 build_ctx: &BuildContext<L::Property>,
1614 self_property: P,
1615 function: &FunctionCallNode,
1616) -> TemplateParseResult<Box<dyn ListTemplate + 'a>>
1617where
1618 L: TemplateLanguage<'a> + ?Sized,
1619 L::Property: WrapTemplateProperty<'a, O>,
1620 P: TemplateProperty + 'a,
1621 P::Output: IntoIterator<Item = O>,
1622 O: Clone + 'a,
1623{
1624 let [lambda_node] = function.expect_exact_arguments()?;
1625 let item_placeholder = PropertyPlaceholder::new();
1626 let item_template =
1627 template_parser::catch_aliases(diagnostics, lambda_node, |diagnostics, node| {
1628 let lambda = template_parser::expect_lambda(node)?;
1629 build_lambda_expression(
1630 build_ctx,
1631 lambda,
1632 &[&|| item_placeholder.clone().into_dyn_wrapped()],
1633 |build_ctx, body| {
1634 expect_template_expression(language, diagnostics, build_ctx, body)
1635 },
1636 )
1637 })?;
1638 let list_template = ListPropertyTemplate::new(
1639 self_property,
1640 Literal(" "), move |formatter, item| {
1642 item_placeholder.with_value(item, || item_template.format(formatter))
1643 },
1644 );
1645 Ok(Box::new(list_template))
1646}
1647
1648fn build_any_operation<'a, L, O, P>(
1651 language: &L,
1652 diagnostics: &mut TemplateDiagnostics,
1653 build_ctx: &BuildContext<L::Property>,
1654 self_property: P,
1655 function: &FunctionCallNode,
1656) -> TemplateParseResult<BoxedTemplateProperty<'a, bool>>
1657where
1658 L: TemplateLanguage<'a> + ?Sized,
1659 L::Property: WrapTemplateProperty<'a, O>,
1660 P: TemplateProperty + 'a,
1661 P::Output: IntoIterator<Item = O>,
1662 O: Clone + 'a,
1663{
1664 let [lambda_node] = function.expect_exact_arguments()?;
1665 let item_placeholder = PropertyPlaceholder::new();
1666 let item_predicate =
1667 template_parser::catch_aliases(diagnostics, lambda_node, |diagnostics, node| {
1668 let lambda = template_parser::expect_lambda(node)?;
1669 build_lambda_expression(
1670 build_ctx,
1671 lambda,
1672 &[&|| item_placeholder.clone().into_dyn_wrapped()],
1673 |build_ctx, body| expect_boolean_expression(language, diagnostics, build_ctx, body),
1674 )
1675 })?;
1676
1677 let out_property = self_property.and_then(move |items| {
1678 items
1679 .into_iter()
1680 .map(|item| item_placeholder.with_value(item, || item_predicate.extract()))
1681 .process_results(|mut predicates| predicates.any(|p| p))
1682 });
1683 Ok(out_property.into_dyn())
1684}
1685
1686fn build_all_operation<'a, L, O, P>(
1689 language: &L,
1690 diagnostics: &mut TemplateDiagnostics,
1691 build_ctx: &BuildContext<L::Property>,
1692 self_property: P,
1693 function: &FunctionCallNode,
1694) -> TemplateParseResult<BoxedTemplateProperty<'a, bool>>
1695where
1696 L: TemplateLanguage<'a> + ?Sized,
1697 L::Property: WrapTemplateProperty<'a, O>,
1698 P: TemplateProperty + 'a,
1699 P::Output: IntoIterator<Item = O>,
1700 O: Clone + 'a,
1701{
1702 let [lambda_node] = function.expect_exact_arguments()?;
1703 let item_placeholder = PropertyPlaceholder::new();
1704 let item_predicate =
1705 template_parser::catch_aliases(diagnostics, lambda_node, |diagnostics, node| {
1706 let lambda = template_parser::expect_lambda(node)?;
1707 build_lambda_expression(
1708 build_ctx,
1709 lambda,
1710 &[&|| item_placeholder.clone().into_dyn_wrapped()],
1711 |build_ctx, body| expect_boolean_expression(language, diagnostics, build_ctx, body),
1712 )
1713 })?;
1714
1715 let out_property = self_property.and_then(move |items| {
1716 items
1717 .into_iter()
1718 .map(|item| item_placeholder.with_value(item, || item_predicate.extract()))
1719 .process_results(|mut predicates| predicates.all(|p| p))
1720 });
1721 Ok(out_property.into_dyn())
1722}
1723
1724fn build_lambda_expression<'i, P, T>(
1727 build_ctx: &BuildContext<'i, P>,
1728 lambda: &LambdaNode<'i>,
1729 arg_fns: &[&'i dyn Fn() -> P],
1730 build_body: impl FnOnce(&BuildContext<'i, P>, &ExpressionNode<'i>) -> TemplateParseResult<T>,
1731) -> TemplateParseResult<T> {
1732 if lambda.params.len() != arg_fns.len() {
1733 return Err(TemplateParseError::expression(
1734 format!("Expected {} lambda parameters", arg_fns.len()),
1735 lambda.params_span,
1736 ));
1737 }
1738 let mut local_variables = build_ctx.local_variables.clone();
1739 local_variables.extend(iter::zip(&lambda.params, arg_fns));
1740 let inner_build_ctx = BuildContext {
1741 local_variables,
1742 self_variable: build_ctx.self_variable,
1743 };
1744 build_body(&inner_build_ctx, &lambda.body)
1745}
1746
1747fn builtin_functions<'a, L: TemplateLanguage<'a> + ?Sized>() -> TemplateBuildFunctionFnMap<'a, L> {
1748 let mut map = TemplateBuildFunctionFnMap::<L>::new();
1751 map.insert("fill", |language, diagnostics, build_ctx, function| {
1752 let [width_node, content_node] = function.expect_exact_arguments()?;
1753 let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
1754 let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1755 let template =
1756 ReformatTemplate::new(content, move |formatter, recorded| match width.extract() {
1757 Ok(width) => text_util::write_wrapped(formatter.as_mut(), recorded, width),
1758 Err(err) => formatter.handle_error(err),
1759 });
1760 Ok(L::Property::wrap_template(Box::new(template)))
1761 });
1762 map.insert("indent", |language, diagnostics, build_ctx, function| {
1763 let [prefix_node, content_node] = function.expect_exact_arguments()?;
1764 let prefix = expect_template_expression(language, diagnostics, build_ctx, prefix_node)?;
1765 let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1766 let template = ReformatTemplate::new(content, move |formatter, recorded| {
1767 let rewrap = formatter.rewrap_fn();
1768 text_util::write_indented(formatter.as_mut(), recorded, |formatter| {
1769 prefix.format(&mut rewrap(formatter))
1770 })
1771 });
1772 Ok(L::Property::wrap_template(Box::new(template)))
1773 });
1774 map.insert("pad_start", |language, diagnostics, build_ctx, function| {
1775 let ([width_node, content_node], [fill_char_node]) =
1776 function.expect_named_arguments(&["", "", "fill_char"])?;
1777 let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
1778 let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1779 let fill_char = fill_char_node
1780 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1781 .transpose()?;
1782 let template = new_pad_template(content, fill_char, width, text_util::write_padded_start);
1783 Ok(L::Property::wrap_template(template))
1784 });
1785 map.insert("pad_end", |language, diagnostics, build_ctx, function| {
1786 let ([width_node, content_node], [fill_char_node]) =
1787 function.expect_named_arguments(&["", "", "fill_char"])?;
1788 let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
1789 let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1790 let fill_char = fill_char_node
1791 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1792 .transpose()?;
1793 let template = new_pad_template(content, fill_char, width, text_util::write_padded_end);
1794 Ok(L::Property::wrap_template(template))
1795 });
1796 map.insert(
1797 "pad_centered",
1798 |language, diagnostics, build_ctx, function| {
1799 let ([width_node, content_node], [fill_char_node]) =
1800 function.expect_named_arguments(&["", "", "fill_char"])?;
1801 let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
1802 let content =
1803 expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1804 let fill_char = fill_char_node
1805 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1806 .transpose()?;
1807 let template =
1808 new_pad_template(content, fill_char, width, text_util::write_padded_centered);
1809 Ok(L::Property::wrap_template(template))
1810 },
1811 );
1812 map.insert(
1813 "truncate_start",
1814 |language, diagnostics, build_ctx, function| {
1815 let ([width_node, content_node], [ellipsis_node]) =
1816 function.expect_named_arguments(&["", "", "ellipsis"])?;
1817 let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
1818 let content =
1819 expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1820 let ellipsis = ellipsis_node
1821 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1822 .transpose()?;
1823 let template =
1824 new_truncate_template(content, ellipsis, width, text_util::write_truncated_start);
1825 Ok(L::Property::wrap_template(template))
1826 },
1827 );
1828 map.insert(
1829 "truncate_end",
1830 |language, diagnostics, build_ctx, function| {
1831 let ([width_node, content_node], [ellipsis_node]) =
1832 function.expect_named_arguments(&["", "", "ellipsis"])?;
1833 let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
1834 let content =
1835 expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1836 let ellipsis = ellipsis_node
1837 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1838 .transpose()?;
1839 let template =
1840 new_truncate_template(content, ellipsis, width, text_util::write_truncated_end);
1841 Ok(L::Property::wrap_template(template))
1842 },
1843 );
1844 map.insert("hash", |language, diagnostics, build_ctx, function| {
1845 let [content_node] = function.expect_exact_arguments()?;
1846 let content = expect_stringify_expression(language, diagnostics, build_ctx, content_node)?;
1847 let result = content.map(|c| hex_util::encode_hex(blake2b_hash(&c).as_ref()));
1848 Ok(result.into_dyn_wrapped())
1849 });
1850 map.insert("label", |language, diagnostics, build_ctx, function| {
1851 let [label_node, content_node] = function.expect_exact_arguments()?;
1852 let label_property =
1853 expect_stringify_expression(language, diagnostics, build_ctx, label_node)?;
1854 let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1855 let labels =
1856 label_property.map(|s| s.split_whitespace().map(ToString::to_string).collect());
1857 Ok(L::Property::wrap_template(Box::new(LabelTemplate::new(
1858 content, labels,
1859 ))))
1860 });
1861 map.insert(
1862 "raw_escape_sequence",
1863 |language, diagnostics, build_ctx, function| {
1864 let [content_node] = function.expect_exact_arguments()?;
1865 let content =
1866 expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1867 Ok(L::Property::wrap_template(Box::new(
1868 RawEscapeSequenceTemplate(content),
1869 )))
1870 },
1871 );
1872 map.insert("hyperlink", |language, diagnostics, build_ctx, function| {
1873 let ([url_node, text_node], [fallback_node]) = function.expect_arguments()?;
1874 let url = expect_stringify_expression(language, diagnostics, build_ctx, url_node)?;
1875 let text = expect_template_expression(language, diagnostics, build_ctx, text_node)?;
1876 let fallback = fallback_node
1877 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1878 .transpose()?;
1879 Ok(L::Property::wrap_template(Box::new(
1880 HyperlinkTemplate::new(url, text, fallback),
1881 )))
1882 });
1883 map.insert("stringify", |language, diagnostics, build_ctx, function| {
1884 let [content_node] = function.expect_exact_arguments()?;
1885 let content = expect_stringify_expression(language, diagnostics, build_ctx, content_node)?;
1886 Ok(L::Property::wrap_property(content))
1887 });
1888 map.insert("json", |language, diagnostics, build_ctx, function| {
1889 let [value_node] = function.expect_exact_arguments()?;
1893 let value = expect_serialize_expression(language, diagnostics, build_ctx, value_node)?;
1894 let out_property = value.and_then(|v| Ok(serde_json::to_string(&v)?));
1895 Ok(out_property.into_dyn_wrapped())
1896 });
1897 map.insert("if", |language, diagnostics, build_ctx, function| {
1898 let ([condition_node, true_node], [false_node]) = function.expect_arguments()?;
1899 let condition =
1900 expect_boolean_expression(language, diagnostics, build_ctx, condition_node)?;
1901 let true_template =
1902 expect_template_expression(language, diagnostics, build_ctx, true_node)?;
1903 let false_template = false_node
1904 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1905 .transpose()?;
1906 let template = ConditionalTemplate::new(condition, true_template, false_template);
1907 Ok(L::Property::wrap_template(Box::new(template)))
1908 });
1909 map.insert("coalesce", |language, diagnostics, build_ctx, function| {
1910 let ([], content_nodes) = function.expect_some_arguments()?;
1911 let contents = content_nodes
1912 .iter()
1913 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1914 .try_collect()?;
1915 Ok(L::Property::wrap_template(Box::new(CoalesceTemplate(
1916 contents,
1917 ))))
1918 });
1919 map.insert("concat", |language, diagnostics, build_ctx, function| {
1920 let ([], content_nodes) = function.expect_some_arguments()?;
1921 let contents = content_nodes
1922 .iter()
1923 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1924 .try_collect()?;
1925 Ok(L::Property::wrap_template(Box::new(ConcatTemplate(
1926 contents,
1927 ))))
1928 });
1929 map.insert("join", |language, diagnostics, build_ctx, function| {
1930 let ([separator_node], content_nodes) = function.expect_some_arguments()?;
1931 let separator =
1932 expect_template_expression(language, diagnostics, build_ctx, separator_node)?;
1933 let contents = content_nodes
1934 .iter()
1935 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1936 .try_collect()?;
1937 Ok(L::Property::wrap_template(Box::new(JoinTemplate::new(
1938 separator, contents,
1939 ))))
1940 });
1941 map.insert("separate", |language, diagnostics, build_ctx, function| {
1942 let ([separator_node], content_nodes) = function.expect_some_arguments()?;
1943 let separator =
1944 expect_template_expression(language, diagnostics, build_ctx, separator_node)?;
1945 let contents = content_nodes
1946 .iter()
1947 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1948 .try_collect()?;
1949 Ok(L::Property::wrap_template(Box::new(SeparateTemplate::new(
1950 separator, contents,
1951 ))))
1952 });
1953 map.insert("surround", |language, diagnostics, build_ctx, function| {
1954 let [prefix_node, suffix_node, content_node] = function.expect_exact_arguments()?;
1955 let prefix = expect_template_expression(language, diagnostics, build_ctx, prefix_node)?;
1956 let suffix = expect_template_expression(language, diagnostics, build_ctx, suffix_node)?;
1957 let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1958 let template = ReformatTemplate::new(content, move |formatter, recorded| {
1959 if recorded.data().is_empty() {
1960 return Ok(());
1961 }
1962 prefix.format(formatter)?;
1963 recorded.replay(formatter.as_mut())?;
1964 suffix.format(formatter)?;
1965 Ok(())
1966 });
1967 Ok(L::Property::wrap_template(Box::new(template)))
1968 });
1969 map.insert("config", |language, diagnostics, _build_ctx, function| {
1970 let [name_node] = function.expect_exact_arguments()?;
1973 let name: ConfigNamePathBuf =
1974 template_parser::catch_aliases(diagnostics, name_node, |_diagnostics, node| {
1975 let name = template_parser::expect_string_literal(node)?;
1976 name.parse().map_err(|err| {
1977 TemplateParseError::expression("Failed to parse config name", node.span)
1978 .with_source(err)
1979 })
1980 })?;
1981 let value = language
1982 .settings()
1983 .get_value(&name)
1984 .optional()
1985 .map_err(|err| {
1986 TemplateParseError::expression("Failed to get config value", function.name_span)
1987 .with_source(err)
1988 })?;
1989 Ok(Literal(value.map(|v| v.decorated("", ""))).into_dyn_wrapped())
1991 });
1992 map
1993}
1994
1995fn new_pad_template<'a, W>(
1996 content: Box<dyn Template + 'a>,
1997 fill_char: Option<Box<dyn Template + 'a>>,
1998 width: BoxedTemplateProperty<'a, usize>,
1999 write_padded: W,
2000) -> Box<dyn Template + 'a>
2001where
2002 W: Fn(&mut dyn Formatter, &FormatRecorder, &FormatRecorder, usize) -> io::Result<()> + 'a,
2003{
2004 let default_fill_char = FormatRecorder::with_data(" ");
2005 let template = ReformatTemplate::new(content, move |formatter, recorded| {
2006 let width = match width.extract() {
2007 Ok(width) => width,
2008 Err(err) => return formatter.handle_error(err),
2009 };
2010 let mut fill_char_recorder;
2011 let recorded_fill_char = if let Some(fill_char) = &fill_char {
2012 let rewrap = formatter.rewrap_fn();
2013 fill_char_recorder = FormatRecorder::new(formatter.maybe_color());
2014 fill_char.format(&mut rewrap(&mut fill_char_recorder))?;
2015 &fill_char_recorder
2016 } else {
2017 &default_fill_char
2018 };
2019 write_padded(formatter.as_mut(), recorded, recorded_fill_char, width)
2020 });
2021 Box::new(template)
2022}
2023
2024fn new_truncate_template<'a, W>(
2025 content: Box<dyn Template + 'a>,
2026 ellipsis: Option<Box<dyn Template + 'a>>,
2027 width: BoxedTemplateProperty<'a, usize>,
2028 write_truncated: W,
2029) -> Box<dyn Template + 'a>
2030where
2031 W: Fn(&mut dyn Formatter, &FormatRecorder, &FormatRecorder, usize) -> io::Result<usize> + 'a,
2032{
2033 let default_ellipsis = FormatRecorder::with_data("");
2034 let template = ReformatTemplate::new(content, move |formatter, recorded| {
2035 let width = match width.extract() {
2036 Ok(width) => width,
2037 Err(err) => return formatter.handle_error(err),
2038 };
2039 let mut ellipsis_recorder;
2040 let recorded_ellipsis = if let Some(ellipsis) = &ellipsis {
2041 let rewrap = formatter.rewrap_fn();
2042 ellipsis_recorder = FormatRecorder::new(formatter.maybe_color());
2043 ellipsis.format(&mut rewrap(&mut ellipsis_recorder))?;
2044 &ellipsis_recorder
2045 } else {
2046 &default_ellipsis
2047 };
2048 write_truncated(formatter.as_mut(), recorded, recorded_ellipsis, width)?;
2049 Ok(())
2050 });
2051 Box::new(template)
2052}
2053
2054pub fn build_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2056 language: &L,
2057 diagnostics: &mut TemplateDiagnostics,
2058 build_ctx: &BuildContext<L::Property>,
2059 node: &ExpressionNode,
2060) -> TemplateParseResult<Expression<L::Property>> {
2061 template_parser::catch_aliases(diagnostics, node, |diagnostics, node| match &node.kind {
2062 ExpressionKind::Identifier(name) => {
2063 if let Some(make) = build_ctx.local_variables.get(name) {
2064 Ok(Expression::unlabeled(make()))
2066 } else if *name == "self" {
2067 let make = build_ctx.self_variable;
2069 Ok(Expression::unlabeled(make()))
2070 } else {
2071 let property = build_keyword(language, diagnostics, build_ctx, name, node.span)
2072 .map_err(|err| {
2073 err.extend_keyword_candidates(itertools::chain(
2074 build_ctx.local_variables.keys().copied(),
2075 ["self"],
2076 ))
2077 })?;
2078 Ok(Expression::with_label(property, *name))
2079 }
2080 }
2081 ExpressionKind::Boolean(value) => {
2082 let property = Literal(*value).into_dyn_wrapped();
2083 Ok(Expression::unlabeled(property))
2084 }
2085 ExpressionKind::Integer(value) => {
2086 let property = Literal(*value).into_dyn_wrapped();
2087 Ok(Expression::unlabeled(property))
2088 }
2089 ExpressionKind::String(value) => {
2090 let property = Literal(value.clone()).into_dyn_wrapped();
2091 Ok(Expression::unlabeled(property))
2092 }
2093 ExpressionKind::Pattern { .. } => Err(TemplateParseError::expression(
2094 "String patterns may not be used as expression values",
2095 node.span,
2096 )),
2097 ExpressionKind::Unary(op, arg_node) => {
2098 let property = build_unary_operation(language, diagnostics, build_ctx, *op, arg_node)?;
2099 Ok(Expression::unlabeled(property))
2100 }
2101 ExpressionKind::Binary(op, lhs_node, rhs_node) => {
2102 let property = build_binary_operation(
2103 language,
2104 diagnostics,
2105 build_ctx,
2106 *op,
2107 lhs_node,
2108 rhs_node,
2109 node.span,
2110 )?;
2111 Ok(Expression::unlabeled(property))
2112 }
2113 ExpressionKind::Concat(nodes) => {
2114 let templates = nodes
2115 .iter()
2116 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
2117 .try_collect()?;
2118 let property = L::Property::wrap_template(Box::new(ConcatTemplate(templates)));
2119 Ok(Expression::unlabeled(property))
2120 }
2121 ExpressionKind::FunctionCall(function) => {
2122 let property = language.build_function(diagnostics, build_ctx, function)?;
2123 Ok(Expression::unlabeled(property))
2124 }
2125 ExpressionKind::MethodCall(method) => {
2126 let mut expression =
2127 build_expression(language, diagnostics, build_ctx, &method.object)?;
2128 expression.property = language.build_method(
2129 diagnostics,
2130 build_ctx,
2131 expression.property,
2132 &method.function,
2133 )?;
2134 expression.labels.push(method.function.name.to_owned());
2135 Ok(expression)
2136 }
2137 ExpressionKind::Lambda(_) => Err(TemplateParseError::expression(
2138 "Lambda cannot be defined here",
2139 node.span,
2140 )),
2141 ExpressionKind::AliasExpanded(..) => unreachable!(),
2142 })
2143}
2144
2145pub fn build<'a, C, L>(
2147 language: &L,
2148 diagnostics: &mut TemplateDiagnostics,
2149 node: &ExpressionNode,
2150) -> TemplateParseResult<TemplateRenderer<'a, C>>
2151where
2152 C: Clone + 'a,
2153 L: TemplateLanguage<'a> + ?Sized,
2154 L::Property: WrapTemplateProperty<'a, C>,
2155{
2156 let self_placeholder = PropertyPlaceholder::new();
2157 let build_ctx = BuildContext {
2158 local_variables: HashMap::new(),
2159 self_variable: &|| self_placeholder.clone().into_dyn_wrapped(),
2160 };
2161 let template = expect_template_expression(language, diagnostics, &build_ctx, node)?;
2162 Ok(TemplateRenderer::new(template, self_placeholder))
2163}
2164
2165pub fn parse<'a, C, L>(
2167 language: &L,
2168 diagnostics: &mut TemplateDiagnostics,
2169 template_text: &str,
2170 aliases_map: &TemplateAliasesMap,
2171) -> TemplateParseResult<TemplateRenderer<'a, C>>
2172where
2173 C: Clone + 'a,
2174 L: TemplateLanguage<'a> + ?Sized,
2175 L::Property: WrapTemplateProperty<'a, C>,
2176{
2177 let node = template_parser::parse(template_text, aliases_map)?;
2178 build(language, diagnostics, &node).map_err(|err| err.extend_alias_candidates(aliases_map))
2179}
2180
2181pub fn expect_boolean_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2182 language: &L,
2183 diagnostics: &mut TemplateDiagnostics,
2184 build_ctx: &BuildContext<L::Property>,
2185 node: &ExpressionNode,
2186) -> TemplateParseResult<BoxedTemplateProperty<'a, bool>> {
2187 expect_expression_of_type(
2188 language,
2189 diagnostics,
2190 build_ctx,
2191 node,
2192 "Boolean",
2193 |expression| expression.try_into_boolean(),
2194 )
2195}
2196
2197pub fn expect_integer_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2198 language: &L,
2199 diagnostics: &mut TemplateDiagnostics,
2200 build_ctx: &BuildContext<L::Property>,
2201 node: &ExpressionNode,
2202) -> TemplateParseResult<BoxedTemplateProperty<'a, i64>> {
2203 expect_expression_of_type(
2204 language,
2205 diagnostics,
2206 build_ctx,
2207 node,
2208 "Integer",
2209 |expression| expression.try_into_integer(),
2210 )
2211}
2212
2213pub fn expect_isize_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2215 language: &L,
2216 diagnostics: &mut TemplateDiagnostics,
2217 build_ctx: &BuildContext<L::Property>,
2218 node: &ExpressionNode,
2219) -> TemplateParseResult<BoxedTemplateProperty<'a, isize>> {
2220 let i64_property = expect_integer_expression(language, diagnostics, build_ctx, node)?;
2221 let isize_property = i64_property.and_then(|v| Ok(isize::try_from(v)?));
2222 Ok(isize_property.into_dyn())
2223}
2224
2225pub fn expect_usize_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2227 language: &L,
2228 diagnostics: &mut TemplateDiagnostics,
2229 build_ctx: &BuildContext<L::Property>,
2230 node: &ExpressionNode,
2231) -> TemplateParseResult<BoxedTemplateProperty<'a, usize>> {
2232 let i64_property = expect_integer_expression(language, diagnostics, build_ctx, node)?;
2233 let usize_property = i64_property.and_then(|v| Ok(usize::try_from(v)?));
2234 Ok(usize_property.into_dyn())
2235}
2236
2237pub fn expect_stringify_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2238 language: &L,
2239 diagnostics: &mut TemplateDiagnostics,
2240 build_ctx: &BuildContext<L::Property>,
2241 node: &ExpressionNode,
2242) -> TemplateParseResult<BoxedTemplateProperty<'a, String>> {
2243 expect_expression_of_type(
2246 language,
2247 diagnostics,
2248 build_ctx,
2249 node,
2250 "Stringify",
2251 |expression| expression.try_into_stringify(),
2252 )
2253}
2254
2255pub fn expect_serialize_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2256 language: &L,
2257 diagnostics: &mut TemplateDiagnostics,
2258 build_ctx: &BuildContext<L::Property>,
2259 node: &ExpressionNode,
2260) -> TemplateParseResult<BoxedSerializeProperty<'a>> {
2261 expect_expression_of_type(
2262 language,
2263 diagnostics,
2264 build_ctx,
2265 node,
2266 "Serialize",
2267 |expression| expression.try_into_serialize(),
2268 )
2269}
2270
2271pub fn expect_template_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2272 language: &L,
2273 diagnostics: &mut TemplateDiagnostics,
2274 build_ctx: &BuildContext<L::Property>,
2275 node: &ExpressionNode,
2276) -> TemplateParseResult<Box<dyn Template + 'a>> {
2277 expect_expression_of_type(
2278 language,
2279 diagnostics,
2280 build_ctx,
2281 node,
2282 "Template",
2283 |expression| expression.try_into_template(),
2284 )
2285}
2286
2287fn expect_expression_of_type<'a, L: TemplateLanguage<'a> + ?Sized, T>(
2288 language: &L,
2289 diagnostics: &mut TemplateDiagnostics,
2290 build_ctx: &BuildContext<L::Property>,
2291 node: &ExpressionNode,
2292 expected_type: &str,
2293 f: impl FnOnce(Expression<L::Property>) -> Option<T>,
2294) -> TemplateParseResult<T> {
2295 template_parser::catch_aliases(diagnostics, node, |diagnostics, node| {
2296 let expression = build_expression(language, diagnostics, build_ctx, node)?;
2297 let actual_type = expression.type_name();
2298 f(expression)
2299 .ok_or_else(|| TemplateParseError::expected_type(expected_type, actual_type, node.span))
2300 })
2301}
2302
2303#[cfg(test)]
2304mod tests {
2305 use assert_matches::assert_matches;
2306 use jj_lib::backend::MillisSinceEpoch;
2307 use jj_lib::config::StackedConfig;
2308
2309 use super::*;
2310 use crate::formatter;
2311 use crate::formatter::ColorFormatter;
2312 use crate::generic_templater;
2313 use crate::generic_templater::GenericTemplateLanguage;
2314
2315 #[derive(Clone, Debug, serde::Serialize)]
2316 struct Context;
2317
2318 type TestTemplateLanguage = GenericTemplateLanguage<'static, Context>;
2319 type TestTemplatePropertyKind = <TestTemplateLanguage as TemplateLanguage<'static>>::Property;
2320
2321 generic_templater::impl_self_property_wrapper!(Context);
2322
2323 struct TestTemplateEnv {
2325 language: TestTemplateLanguage,
2326 aliases_map: TemplateAliasesMap,
2327 color_rules: Vec<(Vec<String>, formatter::Style)>,
2328 }
2329
2330 impl TestTemplateEnv {
2331 fn new() -> Self {
2332 Self::with_config(StackedConfig::with_defaults())
2333 }
2334
2335 fn with_config(config: StackedConfig) -> Self {
2336 let settings = UserSettings::from_config(config).unwrap();
2337 Self {
2338 language: TestTemplateLanguage::new(&settings),
2339 aliases_map: TemplateAliasesMap::new(),
2340 color_rules: Vec::new(),
2341 }
2342 }
2343 }
2344
2345 impl TestTemplateEnv {
2346 fn add_keyword<F>(&mut self, name: &'static str, build: F)
2347 where
2348 F: Fn() -> TestTemplatePropertyKind + 'static,
2349 {
2350 self.language.add_keyword(name, move |_| Ok(build()));
2351 }
2352
2353 fn add_alias(&mut self, decl: impl AsRef<str>, defn: impl Into<String>) {
2354 self.aliases_map.insert(decl, defn).unwrap();
2355 }
2356
2357 fn add_color(&mut self, label: &str, fg: crossterm::style::Color) {
2358 let labels = label.split_whitespace().map(|s| s.to_owned()).collect();
2359 let style = formatter::Style {
2360 fg: Some(fg),
2361 ..Default::default()
2362 };
2363 self.color_rules.push((labels, style));
2364 }
2365
2366 fn parse(&self, template: &str) -> TemplateParseResult<TemplateRenderer<'static, Context>> {
2367 parse(
2368 &self.language,
2369 &mut TemplateDiagnostics::new(),
2370 template,
2371 &self.aliases_map,
2372 )
2373 }
2374
2375 fn parse_err(&self, template: &str) -> String {
2376 let err = self
2377 .parse(template)
2378 .err()
2379 .expect("Got unexpected successful template rendering");
2380
2381 iter::successors(Some(&err as &dyn std::error::Error), |e| e.source()).join("\n")
2382 }
2383
2384 fn parse_err_kind(&self, template: &str) -> TemplateParseErrorKind {
2385 self.parse(template)
2386 .err()
2387 .expect("Got unexpected successful template rendering")
2388 .kind()
2389 .clone()
2390 }
2391
2392 fn render_ok(&self, template: &str) -> String {
2393 let template = self.parse(template).unwrap();
2394 let mut output = Vec::new();
2395 let mut formatter =
2396 ColorFormatter::new(&mut output, self.color_rules.clone().into(), false);
2397 template.format(&Context, &mut formatter).unwrap();
2398 drop(formatter);
2399 String::from_utf8(output).unwrap()
2400 }
2401
2402 fn render_plain(&self, template: &str) -> String {
2403 let template = self.parse(template).unwrap();
2404 String::from_utf8(template.format_plain_text(&Context)).unwrap()
2405 }
2406 }
2407
2408 fn literal<'a, O>(value: O) -> TestTemplatePropertyKind
2409 where
2410 O: Clone + 'a,
2411 TestTemplatePropertyKind: WrapTemplateProperty<'a, O>,
2412 {
2413 Literal(value).into_dyn_wrapped()
2414 }
2415
2416 fn new_error_property<'a, O>(message: &'a str) -> TestTemplatePropertyKind
2417 where
2418 TestTemplatePropertyKind: WrapTemplateProperty<'a, O>,
2419 {
2420 Literal(())
2421 .and_then(|()| Err(TemplatePropertyError(message.into())))
2422 .into_dyn_wrapped()
2423 }
2424
2425 fn new_signature(name: &str, email: &str) -> Signature {
2426 Signature {
2427 name: name.to_owned(),
2428 email: email.to_owned(),
2429 timestamp: new_timestamp(0, 0),
2430 }
2431 }
2432
2433 fn new_timestamp(msec: i64, tz_offset: i32) -> Timestamp {
2434 Timestamp {
2435 timestamp: MillisSinceEpoch(msec),
2436 tz_offset,
2437 }
2438 }
2439
2440 #[test]
2441 fn test_parsed_tree() {
2442 let mut env = TestTemplateEnv::new();
2443 env.add_keyword("divergent", || literal(false));
2444 env.add_keyword("empty", || literal(true));
2445 env.add_keyword("hello", || literal("Hello".to_owned()));
2446
2447 insta::assert_snapshot!(env.render_ok(r#" "#), @"");
2449
2450 insta::assert_snapshot!(env.render_ok(r#" hello.upper() "#), @"HELLO");
2452
2453 insta::assert_snapshot!(env.render_ok(r#" hello.upper() ++ true "#), @"HELLOtrue");
2455
2456 insta::assert_snapshot!(env.render_ok(r#"(hello.upper())"#), @"HELLO");
2458
2459 insta::assert_snapshot!(env.render_ok(r#"(hello.upper() ++ " ") ++ empty"#), @"HELLO true");
2461
2462 insta::assert_snapshot!(env.render_ok(r#"if((divergent), "t", "f")"#), @"f");
2464
2465 insta::assert_snapshot!(env.render_ok(r#"(hello).upper()"#), @"HELLO");
2467
2468 insta::assert_snapshot!(env.render_ok("hello\n .upper()"), @"HELLO");
2470 }
2471
2472 #[test]
2473 fn test_parse_error() {
2474 let mut env = TestTemplateEnv::new();
2475 env.add_keyword("description", || literal("".to_owned()));
2476 env.add_keyword("empty", || literal(true));
2477
2478 insta::assert_snapshot!(env.parse_err(r#"foo bar"#), @r"
2479 --> 1:5
2480 |
2481 1 | foo bar
2482 | ^---
2483 |
2484 = expected <EOI>, `++`, `||`, `&&`, `==`, `!=`, `>=`, `>`, `<=`, `<`, `+`, `-`, `*`, `/`, or `%`
2485 ");
2486 insta::assert_snapshot!(env.parse_err("1 +"), @"
2487 --> 1:4
2488 |
2489 1 | 1 +
2490 | ^---
2491 |
2492 = expected `!`, `-`, or <primary>
2493 ");
2494
2495 insta::assert_snapshot!(env.parse_err(r#"foo"#), @r"
2496 --> 1:1
2497 |
2498 1 | foo
2499 | ^-^
2500 |
2501 = Keyword `foo` doesn't exist
2502 ");
2503
2504 insta::assert_snapshot!(env.parse_err(r#"foo()"#), @r"
2505 --> 1:1
2506 |
2507 1 | foo()
2508 | ^-^
2509 |
2510 = Function `foo` doesn't exist
2511 ");
2512 insta::assert_snapshot!(env.parse_err(r#"false()"#), @r"
2513 --> 1:1
2514 |
2515 1 | false()
2516 | ^---^
2517 |
2518 = Expected identifier
2519 ");
2520
2521 insta::assert_snapshot!(env.parse_err(r#"!foo"#), @r"
2522 --> 1:2
2523 |
2524 1 | !foo
2525 | ^-^
2526 |
2527 = Keyword `foo` doesn't exist
2528 ");
2529 insta::assert_snapshot!(env.parse_err(r#"true && 123"#), @r"
2530 --> 1:9
2531 |
2532 1 | true && 123
2533 | ^-^
2534 |
2535 = Expected expression of type `Boolean`, but actual type is `Integer`
2536 ");
2537 insta::assert_snapshot!(env.parse_err(r#"true == 1"#), @r"
2538 --> 1:1
2539 |
2540 1 | true == 1
2541 | ^-------^
2542 |
2543 = Cannot compare expressions of type `Boolean` and `Integer`
2544 ");
2545 insta::assert_snapshot!(env.parse_err(r#"true != 'a'"#), @r"
2546 --> 1:1
2547 |
2548 1 | true != 'a'
2549 | ^---------^
2550 |
2551 = Cannot compare expressions of type `Boolean` and `String`
2552 ");
2553 insta::assert_snapshot!(env.parse_err(r#"1 == true"#), @r"
2554 --> 1:1
2555 |
2556 1 | 1 == true
2557 | ^-------^
2558 |
2559 = Cannot compare expressions of type `Integer` and `Boolean`
2560 ");
2561 insta::assert_snapshot!(env.parse_err(r#"1 != 'a'"#), @r"
2562 --> 1:1
2563 |
2564 1 | 1 != 'a'
2565 | ^------^
2566 |
2567 = Cannot compare expressions of type `Integer` and `String`
2568 ");
2569 insta::assert_snapshot!(env.parse_err(r#"'a' == true"#), @r"
2570 --> 1:1
2571 |
2572 1 | 'a' == true
2573 | ^---------^
2574 |
2575 = Cannot compare expressions of type `String` and `Boolean`
2576 ");
2577 insta::assert_snapshot!(env.parse_err(r#"'a' != 1"#), @r"
2578 --> 1:1
2579 |
2580 1 | 'a' != 1
2581 | ^------^
2582 |
2583 = Cannot compare expressions of type `String` and `Integer`
2584 ");
2585 insta::assert_snapshot!(env.parse_err(r#"'a' == label("", "")"#), @r#"
2586 --> 1:1
2587 |
2588 1 | 'a' == label("", "")
2589 | ^------------------^
2590 |
2591 = Cannot compare expressions of type `String` and `Template`
2592 "#);
2593 insta::assert_snapshot!(env.parse_err(r#"'a' > 1"#), @r"
2594 --> 1:1
2595 |
2596 1 | 'a' > 1
2597 | ^-----^
2598 |
2599 = Cannot compare expressions of type `String` and `Integer`
2600 ");
2601
2602 insta::assert_snapshot!(env.parse_err(r#"description.first_line().foo()"#), @r"
2603 --> 1:26
2604 |
2605 1 | description.first_line().foo()
2606 | ^-^
2607 |
2608 = Method `foo` doesn't exist for type `String`
2609 ");
2610
2611 insta::assert_snapshot!(env.parse_err(r#"10000000000000000000"#), @r"
2612 --> 1:1
2613 |
2614 1 | 10000000000000000000
2615 | ^------------------^
2616 |
2617 = Invalid integer literal
2618 number too large to fit in target type
2619 ");
2620 insta::assert_snapshot!(env.parse_err(r#"42.foo()"#), @r"
2621 --> 1:4
2622 |
2623 1 | 42.foo()
2624 | ^-^
2625 |
2626 = Method `foo` doesn't exist for type `Integer`
2627 ");
2628 insta::assert_snapshot!(env.parse_err(r#"(-empty)"#), @r"
2629 --> 1:3
2630 |
2631 1 | (-empty)
2632 | ^---^
2633 |
2634 = Expected expression of type `Integer`, but actual type is `Boolean`
2635 ");
2636
2637 insta::assert_snapshot!(env.parse_err(r#"("foo" ++ "bar").baz()"#), @r#"
2638 --> 1:18
2639 |
2640 1 | ("foo" ++ "bar").baz()
2641 | ^-^
2642 |
2643 = Method `baz` doesn't exist for type `Template`
2644 "#);
2645
2646 insta::assert_snapshot!(env.parse_err(r#"description.contains()"#), @r"
2647 --> 1:22
2648 |
2649 1 | description.contains()
2650 | ^
2651 |
2652 = Function `contains`: Expected 1 arguments
2653 ");
2654
2655 insta::assert_snapshot!(env.parse_err(r#"description.first_line("foo")"#), @r#"
2656 --> 1:24
2657 |
2658 1 | description.first_line("foo")
2659 | ^---^
2660 |
2661 = Function `first_line`: Expected 0 arguments
2662 "#);
2663
2664 insta::assert_snapshot!(env.parse_err(r#"label()"#), @r"
2665 --> 1:7
2666 |
2667 1 | label()
2668 | ^
2669 |
2670 = Function `label`: Expected 2 arguments
2671 ");
2672 insta::assert_snapshot!(env.parse_err(r#"label("foo", "bar", "baz")"#), @r#"
2673 --> 1:7
2674 |
2675 1 | label("foo", "bar", "baz")
2676 | ^-----------------^
2677 |
2678 = Function `label`: Expected 2 arguments
2679 "#);
2680
2681 insta::assert_snapshot!(env.parse_err(r#"if()"#), @r"
2682 --> 1:4
2683 |
2684 1 | if()
2685 | ^
2686 |
2687 = Function `if`: Expected 2 to 3 arguments
2688 ");
2689 insta::assert_snapshot!(env.parse_err(r#"if("foo", "bar", "baz", "quux")"#), @r#"
2690 --> 1:4
2691 |
2692 1 | if("foo", "bar", "baz", "quux")
2693 | ^-------------------------^
2694 |
2695 = Function `if`: Expected 2 to 3 arguments
2696 "#);
2697
2698 insta::assert_snapshot!(env.parse_err(r#"pad_start("foo", fill_char = "bar", "baz")"#), @r#"
2699 --> 1:37
2700 |
2701 1 | pad_start("foo", fill_char = "bar", "baz")
2702 | ^---^
2703 |
2704 = Function `pad_start`: Positional argument follows keyword argument
2705 "#);
2706
2707 insta::assert_snapshot!(env.parse_err(r#"if(label("foo", "bar"), "baz")"#), @r#"
2708 --> 1:4
2709 |
2710 1 | if(label("foo", "bar"), "baz")
2711 | ^-----------------^
2712 |
2713 = Expected expression of type `Boolean`, but actual type is `Template`
2714 "#);
2715
2716 insta::assert_snapshot!(env.parse_err(r#"|x| description"#), @r"
2717 --> 1:1
2718 |
2719 1 | |x| description
2720 | ^-------------^
2721 |
2722 = Lambda cannot be defined here
2723 ");
2724 }
2725
2726 #[test]
2727 fn test_self_keyword() {
2728 let mut env = TestTemplateEnv::new();
2729 env.add_keyword("say_hello", || literal("Hello".to_owned()));
2730
2731 insta::assert_snapshot!(env.render_ok(r#"self.say_hello()"#), @"Hello");
2732 insta::assert_snapshot!(env.parse_err(r#"self"#), @r"
2733 --> 1:1
2734 |
2735 1 | self
2736 | ^--^
2737 |
2738 = Expected expression of type `Template`, but actual type is `Self`
2739 ");
2740 }
2741
2742 #[test]
2743 fn test_boolean_cast() {
2744 let mut env = TestTemplateEnv::new();
2745
2746 insta::assert_snapshot!(env.render_ok(r#"if("", true, false)"#), @"false");
2747 insta::assert_snapshot!(env.render_ok(r#"if("a", true, false)"#), @"true");
2748
2749 env.add_keyword("sl0", || literal::<Vec<String>>(vec![]));
2750 env.add_keyword("sl1", || literal(vec!["".to_owned()]));
2751 insta::assert_snapshot!(env.render_ok(r#"if(sl0, true, false)"#), @"false");
2752 insta::assert_snapshot!(env.render_ok(r#"if(sl1, true, false)"#), @"true");
2753
2754 insta::assert_snapshot!(env.parse_err(r#"if(0, true, false)"#), @r"
2756 --> 1:4
2757 |
2758 1 | if(0, true, false)
2759 | ^
2760 |
2761 = Expected expression of type `Boolean`, but actual type is `Integer`
2762 ");
2763
2764 env.add_keyword("none_i64", || literal(None::<i64>));
2766 env.add_keyword("some_i64", || literal(Some(0)));
2767 insta::assert_snapshot!(env.render_ok(r#"if(none_i64, true, false)"#), @"false");
2768 insta::assert_snapshot!(env.render_ok(r#"if(some_i64, true, false)"#), @"true");
2769
2770 insta::assert_snapshot!(
2772 env.render_ok("if(-none_i64 == 1, true, false)"),
2773 @"<Error: No Integer available>"
2774 );
2775
2776 insta::assert_snapshot!(env.parse_err(r#"if(label("", ""), true, false)"#), @r#"
2777 --> 1:4
2778 |
2779 1 | if(label("", ""), true, false)
2780 | ^-----------^
2781 |
2782 = Expected expression of type `Boolean`, but actual type is `Template`
2783 "#);
2784 insta::assert_snapshot!(env.parse_err(r#"if(sl0.map(|x| x), true, false)"#), @r"
2785 --> 1:4
2786 |
2787 1 | if(sl0.map(|x| x), true, false)
2788 | ^------------^
2789 |
2790 = Expected expression of type `Boolean`, but actual type is `ListTemplate`
2791 ");
2792
2793 env.add_keyword("empty_email", || literal(Email("".to_owned())));
2794 env.add_keyword("nonempty_email", || {
2795 literal(Email("local@domain".to_owned()))
2796 });
2797 insta::assert_snapshot!(env.render_ok(r#"if(empty_email, true, false)"#), @"false");
2798 insta::assert_snapshot!(env.render_ok(r#"if(nonempty_email, true, false)"#), @"true");
2799
2800 env.add_keyword("config_bool", || literal(ConfigValue::from(true)));
2802 insta::assert_snapshot!(env.parse_err("if(config_bool, true, false)"), @"
2803 --> 1:4
2804 |
2805 1 | if(config_bool, true, false)
2806 | ^---------^
2807 |
2808 = Expected expression of type `Boolean`, but actual type is `ConfigValue`
2809 ");
2810
2811 env.add_keyword("signature", || {
2813 literal(new_signature("Test User", "test.user@example.com"))
2814 });
2815 env.add_keyword("size_hint", || literal((5, None)));
2816 env.add_keyword("timestamp", || literal(new_timestamp(0, 0)));
2817 env.add_keyword("timestamp_range", || {
2818 literal(TimestampRange {
2819 start: new_timestamp(0, 0),
2820 end: new_timestamp(0, 0),
2821 })
2822 });
2823 assert_matches!(
2824 env.parse_err_kind("if(signature, true, false)"),
2825 TemplateParseErrorKind::Expression(_)
2826 );
2827 assert_matches!(
2828 env.parse_err_kind("if(size_hint, true, false)"),
2829 TemplateParseErrorKind::Expression(_)
2830 );
2831 assert_matches!(
2832 env.parse_err_kind("if(timestamp, true, false)"),
2833 TemplateParseErrorKind::Expression(_)
2834 );
2835 assert_matches!(
2836 env.parse_err_kind("if(timestamp_range, true, false)"),
2837 TemplateParseErrorKind::Expression(_)
2838 );
2839 }
2840
2841 #[test]
2842 fn test_arithmetic_operation() {
2843 let mut env = TestTemplateEnv::new();
2844 env.add_keyword("none_i64", || literal(None::<i64>));
2845 env.add_keyword("some_i64", || literal(Some(1)));
2846 env.add_keyword("i64_min", || literal(i64::MIN));
2847 env.add_keyword("i64_max", || literal(i64::MAX));
2848
2849 insta::assert_snapshot!(env.render_ok(r#"-1"#), @"-1");
2850 insta::assert_snapshot!(env.render_ok(r#"--2"#), @"2");
2851 insta::assert_snapshot!(env.render_ok(r#"-(3)"#), @"-3");
2852 insta::assert_snapshot!(env.render_ok(r#"1 + 2"#), @"3");
2853 insta::assert_snapshot!(env.render_ok(r#"2 * 3"#), @"6");
2854 insta::assert_snapshot!(env.render_ok(r#"1 + 2 * 3"#), @"7");
2855 insta::assert_snapshot!(env.render_ok(r#"4 / 2"#), @"2");
2856 insta::assert_snapshot!(env.render_ok(r#"5 / 2"#), @"2");
2857 insta::assert_snapshot!(env.render_ok(r#"5 % 2"#), @"1");
2858
2859 insta::assert_snapshot!(env.render_ok(r#"-none_i64"#), @"<Error: No Integer available>");
2862 insta::assert_snapshot!(env.render_ok(r#"-some_i64"#), @"-1");
2863 insta::assert_snapshot!(env.render_ok(r#"some_i64 + some_i64"#), @"2");
2864 insta::assert_snapshot!(env.render_ok(r#"some_i64 + none_i64"#), @"<Error: No Integer available>");
2865 insta::assert_snapshot!(env.render_ok(r#"none_i64 + some_i64"#), @"<Error: No Integer available>");
2866 insta::assert_snapshot!(env.render_ok(r#"none_i64 + none_i64"#), @"<Error: No Integer available>");
2867
2868 insta::assert_snapshot!(
2870 env.render_ok(r#"-i64_min"#),
2871 @"<Error: Attempt to negate with overflow>");
2872 insta::assert_snapshot!(
2873 env.render_ok(r#"i64_max + 1"#),
2874 @"<Error: Attempt to add with overflow>");
2875 insta::assert_snapshot!(
2876 env.render_ok(r#"i64_min - 1"#),
2877 @"<Error: Attempt to subtract with overflow>");
2878 insta::assert_snapshot!(
2879 env.render_ok(r#"i64_max * 2"#),
2880 @"<Error: Attempt to multiply with overflow>");
2881 insta::assert_snapshot!(
2882 env.render_ok(r#"i64_min / -1"#),
2883 @"<Error: Attempt to divide with overflow>");
2884 insta::assert_snapshot!(
2885 env.render_ok(r#"1 / 0"#),
2886 @"<Error: Attempt to divide by zero>");
2887 insta::assert_snapshot!(
2888 env.render_ok("i64_min % -1"),
2889 @"<Error: Attempt to divide with overflow>");
2890 insta::assert_snapshot!(
2891 env.render_ok(r#"1 % 0"#),
2892 @"<Error: Attempt to divide by zero>");
2893 }
2894
2895 #[test]
2896 fn test_relational_operation() {
2897 let mut env = TestTemplateEnv::new();
2898 env.add_keyword("none_i64", || literal(None::<i64>));
2899 env.add_keyword("some_i64_0", || literal(Some(0_i64)));
2900 env.add_keyword("some_i64_1", || literal(Some(1_i64)));
2901
2902 insta::assert_snapshot!(env.render_ok(r#"1 >= 1"#), @"true");
2903 insta::assert_snapshot!(env.render_ok(r#"0 >= 1"#), @"false");
2904 insta::assert_snapshot!(env.render_ok(r#"2 > 1"#), @"true");
2905 insta::assert_snapshot!(env.render_ok(r#"1 > 1"#), @"false");
2906 insta::assert_snapshot!(env.render_ok(r#"1 <= 1"#), @"true");
2907 insta::assert_snapshot!(env.render_ok(r#"2 <= 1"#), @"false");
2908 insta::assert_snapshot!(env.render_ok(r#"0 < 1"#), @"true");
2909 insta::assert_snapshot!(env.render_ok(r#"1 < 1"#), @"false");
2910
2911 insta::assert_snapshot!(env.render_ok(r#"none_i64 < some_i64_0"#), @"true");
2913 insta::assert_snapshot!(env.render_ok(r#"some_i64_0 > some_i64_1"#), @"false");
2914 insta::assert_snapshot!(env.render_ok(r#"none_i64 < 0"#), @"true");
2915 insta::assert_snapshot!(env.render_ok(r#"1 > some_i64_0"#), @"true");
2916
2917 assert_matches!(
2919 env.parse_err_kind("42 >= true"),
2920 TemplateParseErrorKind::Expression(_)
2921 );
2922 assert_matches!(
2923 env.parse_err_kind("none_i64 >= true"),
2924 TemplateParseErrorKind::Expression(_)
2925 );
2926
2927 env.add_keyword("str_list", || {
2929 literal(vec!["foo".to_owned(), "bar".to_owned()])
2930 });
2931 env.add_keyword("cfg_val", || {
2932 literal(ConfigValue::from_iter([("foo", "bar")]))
2933 });
2934 env.add_keyword("signature", || {
2935 literal(new_signature("User", "user@example.com"))
2936 });
2937 env.add_keyword("email", || literal(Email("me@example.com".to_owned())));
2938 env.add_keyword("size_hint", || literal((10, None)));
2939 env.add_keyword("timestamp", || literal(new_timestamp(0, 0)));
2940 env.add_keyword("timestamp_range", || {
2941 literal(TimestampRange {
2942 start: new_timestamp(0, 0),
2943 end: new_timestamp(0, 0),
2944 })
2945 });
2946 assert_matches!(
2947 env.parse_err_kind("'a' >= 'a'"),
2948 TemplateParseErrorKind::Expression(_)
2949 );
2950 assert_matches!(
2951 env.parse_err_kind("str_list >= str_list"),
2952 TemplateParseErrorKind::Expression(_)
2953 );
2954 assert_matches!(
2955 env.parse_err_kind("true >= true"),
2956 TemplateParseErrorKind::Expression(_)
2957 );
2958 assert_matches!(
2959 env.parse_err_kind("cfg_val >= cfg_val"),
2960 TemplateParseErrorKind::Expression(_)
2961 );
2962 assert_matches!(
2963 env.parse_err_kind("signature >= signature"),
2964 TemplateParseErrorKind::Expression(_)
2965 );
2966 assert_matches!(
2967 env.parse_err_kind("email >= email"),
2968 TemplateParseErrorKind::Expression(_)
2969 );
2970 assert_matches!(
2971 env.parse_err_kind("size_hint >= size_hint"),
2972 TemplateParseErrorKind::Expression(_)
2973 );
2974 assert_matches!(
2975 env.parse_err_kind("timestamp >= timestamp"),
2976 TemplateParseErrorKind::Expression(_)
2977 );
2978 assert_matches!(
2979 env.parse_err_kind("timestamp_range >= timestamp_range"),
2980 TemplateParseErrorKind::Expression(_)
2981 );
2982 assert_matches!(
2983 env.parse_err_kind("label('', '') >= label('', '')"),
2984 TemplateParseErrorKind::Expression(_)
2985 );
2986 assert_matches!(
2987 env.parse_err_kind("str_list.map(|s| s) >= str_list.map(|s| s)"),
2988 TemplateParseErrorKind::Expression(_)
2989 );
2990 }
2991
2992 #[test]
2993 fn test_logical_operation() {
2994 let mut env = TestTemplateEnv::new();
2995 env.add_keyword("none_i64", || literal::<Option<i64>>(None));
2996 env.add_keyword("some_i64_0", || literal(Some(0_i64)));
2997 env.add_keyword("some_i64_1", || literal(Some(1_i64)));
2998 env.add_keyword("email1", || literal(Email("local-1@domain".to_owned())));
2999 env.add_keyword("email2", || literal(Email("local-2@domain".to_owned())));
3000
3001 insta::assert_snapshot!(env.render_ok(r#"!false"#), @"true");
3002 insta::assert_snapshot!(env.render_ok(r#"false || !false"#), @"true");
3003 insta::assert_snapshot!(env.render_ok(r#"false && true"#), @"false");
3004 insta::assert_snapshot!(env.render_ok(r#"true == true"#), @"true");
3005 insta::assert_snapshot!(env.render_ok(r#"true == false"#), @"false");
3006 insta::assert_snapshot!(env.render_ok(r#"true != true"#), @"false");
3007 insta::assert_snapshot!(env.render_ok(r#"true != false"#), @"true");
3008
3009 insta::assert_snapshot!(env.render_ok(r#"1 == 1"#), @"true");
3010 insta::assert_snapshot!(env.render_ok(r#"1 == 2"#), @"false");
3011 insta::assert_snapshot!(env.render_ok(r#"1 != 1"#), @"false");
3012 insta::assert_snapshot!(env.render_ok(r#"1 != 2"#), @"true");
3013 insta::assert_snapshot!(env.render_ok(r#"none_i64 == none_i64"#), @"true");
3014 insta::assert_snapshot!(env.render_ok(r#"some_i64_0 != some_i64_0"#), @"false");
3015 insta::assert_snapshot!(env.render_ok(r#"none_i64 == 0"#), @"false");
3016 insta::assert_snapshot!(env.render_ok(r#"some_i64_0 != 0"#), @"false");
3017 insta::assert_snapshot!(env.render_ok(r#"1 == some_i64_1"#), @"true");
3018
3019 insta::assert_snapshot!(env.render_ok(r#"'a' == 'a'"#), @"true");
3020 insta::assert_snapshot!(env.render_ok(r#"'a' == 'b'"#), @"false");
3021 insta::assert_snapshot!(env.render_ok(r#"'a' != 'a'"#), @"false");
3022 insta::assert_snapshot!(env.render_ok(r#"'a' != 'b'"#), @"true");
3023 insta::assert_snapshot!(env.render_ok(r#"email1 == email1"#), @"true");
3024 insta::assert_snapshot!(env.render_ok(r#"email1 == email2"#), @"false");
3025 insta::assert_snapshot!(env.render_ok(r#"email1 == 'local-1@domain'"#), @"true");
3026 insta::assert_snapshot!(env.render_ok(r#"email1 != 'local-2@domain'"#), @"true");
3027 insta::assert_snapshot!(env.render_ok(r#"'local-1@domain' == email1"#), @"true");
3028 insta::assert_snapshot!(env.render_ok(r#"'local-2@domain' != email1"#), @"true");
3029
3030 insta::assert_snapshot!(env.render_ok(r#" !"" "#), @"true");
3031 insta::assert_snapshot!(env.render_ok(r#" "" || "a".lines() "#), @"true");
3032
3033 env.add_keyword("bad_bool", || new_error_property::<bool>("Bad"));
3035 insta::assert_snapshot!(env.render_ok(r#"false && bad_bool"#), @"false");
3036 insta::assert_snapshot!(env.render_ok(r#"true && bad_bool"#), @"<Error: Bad>");
3037 insta::assert_snapshot!(env.render_ok(r#"false || bad_bool"#), @"<Error: Bad>");
3038 insta::assert_snapshot!(env.render_ok(r#"true || bad_bool"#), @"true");
3039
3040 assert_matches!(
3042 env.parse_err_kind("some_i64_0 == '0'"),
3043 TemplateParseErrorKind::Expression(_)
3044 );
3045 assert_matches!(
3046 env.parse_err_kind("email1 == 42"),
3047 TemplateParseErrorKind::Expression(_)
3048 );
3049
3050 env.add_keyword("str_list", || {
3052 literal(vec!["foo".to_owned(), "bar".to_owned()])
3053 });
3054 env.add_keyword("cfg_val", || {
3055 literal(ConfigValue::from_iter([("foo", "bar")]))
3056 });
3057 env.add_keyword("signature", || {
3058 literal(new_signature("User", "user@example.com"))
3059 });
3060 env.add_keyword("size_hint", || literal((10, None)));
3061 env.add_keyword("timestamp", || literal(new_timestamp(0, 0)));
3062 env.add_keyword("timestamp_range", || {
3063 literal(TimestampRange {
3064 start: new_timestamp(0, 0),
3065 end: new_timestamp(0, 0),
3066 })
3067 });
3068 assert_matches!(
3069 env.parse_err_kind("str_list == str_list"),
3070 TemplateParseErrorKind::Expression(_)
3071 );
3072 assert_matches!(
3073 env.parse_err_kind("cfg_val == cfg_val"),
3074 TemplateParseErrorKind::Expression(_)
3075 );
3076 assert_matches!(
3077 env.parse_err_kind("signature == signature"),
3078 TemplateParseErrorKind::Expression(_)
3079 );
3080 assert_matches!(
3081 env.parse_err_kind("size_hint == size_hint"),
3082 TemplateParseErrorKind::Expression(_)
3083 );
3084 assert_matches!(
3085 env.parse_err_kind("timestamp == timestamp"),
3086 TemplateParseErrorKind::Expression(_)
3087 );
3088 assert_matches!(
3089 env.parse_err_kind("timestamp_range == timestamp_range"),
3090 TemplateParseErrorKind::Expression(_)
3091 );
3092 assert_matches!(
3093 env.parse_err_kind("label('', '') == label('', '')"),
3094 TemplateParseErrorKind::Expression(_)
3095 );
3096 assert_matches!(
3097 env.parse_err_kind("str_list.map(|s| s) == str_list.map(|s| s)"),
3098 TemplateParseErrorKind::Expression(_)
3099 );
3100 }
3101
3102 #[test]
3103 fn test_list_method() {
3104 let mut env = TestTemplateEnv::new();
3105 env.add_keyword("empty", || literal(true));
3106 env.add_keyword("sep", || literal("sep".to_owned()));
3107
3108 insta::assert_snapshot!(env.render_ok(r#""".lines().len()"#), @"0");
3109 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().len()"#), @"3");
3110
3111 insta::assert_snapshot!(env.render_ok(r#""".lines().join("|")"#), @"");
3112 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().join("|")"#), @"a|b|c");
3113 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().join("\0")"#), @"a\0b\0c");
3115 insta::assert_snapshot!(
3117 env.render_ok(r#""a\nb\nc".lines().join(sep.upper())"#),
3118 @"aSEPbSEPc");
3119
3120 insta::assert_snapshot!(
3121 env.render_ok(r#""a\nbb\nc".lines().filter(|s| s.len() == 1)"#),
3122 @"a c");
3123
3124 insta::assert_snapshot!(
3125 env.render_ok(r#""a\nb\nc".lines().map(|s| s ++ s)"#),
3126 @"aa bb cc");
3127
3128 insta::assert_snapshot!(
3130 env.render_ok(r#""a\nb\nc".lines().any(|s| s == "b")"#),
3131 @"true");
3132 insta::assert_snapshot!(
3133 env.render_ok(r#""a\nb\nc".lines().any(|s| s == "d")"#),
3134 @"false");
3135 insta::assert_snapshot!(
3136 env.render_ok(r#""".lines().any(|s| s == "a")"#),
3137 @"false");
3138 insta::assert_snapshot!(
3140 env.render_ok(r#""ax\nbb\nc".lines().any(|s| s.contains("x"))"#),
3141 @"true");
3142 insta::assert_snapshot!(
3143 env.render_ok(r#""a\nbb\nc".lines().any(|s| s.len() > 1)"#),
3144 @"true");
3145
3146 insta::assert_snapshot!(
3148 env.render_ok(r#""a\nb\nc".lines().all(|s| s.len() == 1)"#),
3149 @"true");
3150 insta::assert_snapshot!(
3151 env.render_ok(r#""a\nbb\nc".lines().all(|s| s.len() == 1)"#),
3152 @"false");
3153 insta::assert_snapshot!(
3155 env.render_ok(r#""".lines().all(|s| s == "a")"#),
3156 @"true");
3157 insta::assert_snapshot!(
3159 env.render_ok(r#""ax\nbx\ncx".lines().all(|s| s.ends_with("x"))"#),
3160 @"true");
3161 insta::assert_snapshot!(
3162 env.render_ok(r#""a\nbb\nc".lines().all(|s| s.len() < 3)"#),
3163 @"true");
3164
3165 insta::assert_snapshot!(
3167 env.render_ok(r#""a\nbb\nccc".lines().filter(|s| s.len() > 1).any(|s| s == "bb")"#),
3168 @"true");
3169 insta::assert_snapshot!(
3170 env.render_ok(r#""a\nbb\nccc".lines().filter(|s| s.len() > 1).all(|s| s.len() >= 2)"#),
3171 @"true");
3172
3173 insta::assert_snapshot!(
3175 env.render_ok(r#"if("a\nb".lines().any(|s| s == "a"), "found", "not found")"#),
3176 @"found");
3177 insta::assert_snapshot!(
3178 env.render_ok(r#"if("a\nb".lines().all(|s| s.len() == 1), "all single", "not all")"#),
3179 @"all single");
3180
3181 insta::assert_snapshot!(
3183 env.render_ok(r#""a\nb\nc".lines().map(|s| s ++ empty)"#),
3184 @"atrue btrue ctrue");
3185 insta::assert_snapshot!(
3187 env.render_ok(r#""a\nb\nc".lines().map(|self| self ++ empty)"#),
3188 @"atrue btrue ctrue");
3189 insta::assert_snapshot!(
3191 env.render_ok(r#""a\nb\nc".lines().map(|empty| empty)"#),
3192 @"a b c");
3193 insta::assert_snapshot!(
3195 env.render_ok(r#""a\nb\nc".lines().map(|s| "x\ny".lines().map(|t| s ++ t))"#),
3196 @"ax ay bx by cx cy");
3197 insta::assert_snapshot!(
3199 env.render_ok(r#""a\nb\nc".lines().map(|s| "x\ny".lines().map(|t| s ++ t).join(",")).join(";")"#),
3200 @"ax,ay;bx,by;cx,cy");
3201 insta::assert_snapshot!(
3203 env.render_ok(r#""! a\n!b\nc\n end".remove_suffix("end").trim_end().lines().map(|s| s.remove_prefix("!").trim_start())"#),
3204 @"a b c");
3205
3206 env.add_alias("identity", "|x| x");
3208 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().map(identity)"#), @"a b c");
3209
3210 insta::assert_snapshot!(env.parse_err(r#""a".lines().map(empty)"#), @r#"
3212 --> 1:17
3213 |
3214 1 | "a".lines().map(empty)
3215 | ^---^
3216 |
3217 = Expected lambda expression
3218 "#);
3219 insta::assert_snapshot!(env.parse_err(r#""a".lines().map(|| "")"#), @r#"
3221 --> 1:18
3222 |
3223 1 | "a".lines().map(|| "")
3224 | ^
3225 |
3226 = Expected 1 lambda parameters
3227 "#);
3228 insta::assert_snapshot!(env.parse_err(r#""a".lines().map(|a, b| "")"#), @r#"
3229 --> 1:18
3230 |
3231 1 | "a".lines().map(|a, b| "")
3232 | ^--^
3233 |
3234 = Expected 1 lambda parameters
3235 "#);
3236 insta::assert_snapshot!(env.parse_err(r#""a".lines().filter(|s| s ++ "\n")"#), @r#"
3238 --> 1:24
3239 |
3240 1 | "a".lines().filter(|s| s ++ "\n")
3241 | ^-------^
3242 |
3243 = Expected expression of type `Boolean`, but actual type is `Template`
3244 "#);
3245
3246 insta::assert_snapshot!(env.parse_err(r#""a".lines().any(|s| s.len())"#), @r#"
3248 --> 1:21
3249 |
3250 1 | "a".lines().any(|s| s.len())
3251 | ^-----^
3252 |
3253 = Expected expression of type `Boolean`, but actual type is `Integer`
3254 "#);
3255 insta::assert_snapshot!(env.parse_err(r#""a".lines().all(|s| s ++ "x")"#), @r#"
3257 --> 1:21
3258 |
3259 1 | "a".lines().all(|s| s ++ "x")
3260 | ^------^
3261 |
3262 = Expected expression of type `Boolean`, but actual type is `Template`
3263 "#);
3264 insta::assert_snapshot!(env.parse_err(r#""a".lines().any(|| true)"#), @r#"
3266 --> 1:18
3267 |
3268 1 | "a".lines().any(|| true)
3269 | ^
3270 |
3271 = Expected 1 lambda parameters
3272 "#);
3273 insta::assert_snapshot!(env.parse_err(r#""a".lines().all(|a, b| true)"#), @r#"
3275 --> 1:18
3276 |
3277 1 | "a".lines().all(|a, b| true)
3278 | ^--^
3279 |
3280 = Expected 1 lambda parameters
3281 "#);
3282 insta::assert_snapshot!(env.parse_err(r#""a".lines().map(|s| s.unknown())"#), @r#"
3284 --> 1:23
3285 |
3286 1 | "a".lines().map(|s| s.unknown())
3287 | ^-----^
3288 |
3289 = Method `unknown` doesn't exist for type `String`
3290 "#);
3291 env.add_alias("too_many_params", "|x, y| x");
3293 insta::assert_snapshot!(env.parse_err(r#""a".lines().map(too_many_params)"#), @r#"
3294 --> 1:17
3295 |
3296 1 | "a".lines().map(too_many_params)
3297 | ^-------------^
3298 |
3299 = In alias `too_many_params`
3300 --> 1:2
3301 |
3302 1 | |x, y| x
3303 | ^--^
3304 |
3305 = Expected 1 lambda parameters
3306 "#);
3307 }
3308
3309 #[test]
3310 fn test_string_method() {
3311 let mut env = TestTemplateEnv::new();
3312 env.add_keyword("description", || literal("description 1".to_owned()));
3313 env.add_keyword("bad_string", || new_error_property::<String>("Bad"));
3314
3315 insta::assert_snapshot!(env.render_ok(r#""".len()"#), @"0");
3316 insta::assert_snapshot!(env.render_ok(r#""foo".len()"#), @"3");
3317 insta::assert_snapshot!(env.render_ok(r#""💩".len()"#), @"4");
3318
3319 insta::assert_snapshot!(env.render_ok(r#""fooo".contains("foo")"#), @"true");
3320 insta::assert_snapshot!(env.render_ok(r#""foo".contains("fooo")"#), @"false");
3321 insta::assert_snapshot!(env.render_ok(r#"description.contains("description")"#), @"true");
3322 insta::assert_snapshot!(
3323 env.render_ok(r#""description 123".contains(description.first_line())"#),
3324 @"true");
3325
3326 insta::assert_snapshot!(env.parse_err(r#""fa".starts_with(regex:'[a-f]o+')"#), @r#"
3328 --> 1:18
3329 |
3330 1 | "fa".starts_with(regex:'[a-f]o+')
3331 | ^-------------^
3332 |
3333 = String patterns may not be used as expression values
3334 "#);
3335
3336 insta::assert_snapshot!(env.render_ok(r#""foo".contains(bad_string)"#), @"<Error: Bad>");
3338 insta::assert_snapshot!(
3339 env.render_ok(r#""foo".contains("f" ++ bad_string) ++ "bar""#), @"<Error: Bad>bar");
3340 insta::assert_snapshot!(
3341 env.render_ok(r#""foo".contains(separate("o", "f", bad_string))"#), @"<Error: Bad>");
3342
3343 insta::assert_snapshot!(env.render_ok(r#""fooo".match(regex:'[a-f]o+')"#), @"fooo");
3344 insta::assert_snapshot!(env.render_ok(r#""fa".match(regex:'[a-f]o+')"#), @"");
3345 insta::assert_snapshot!(env.render_ok(r#""hello".match(regex:"h(ell)o")"#), @"hello");
3346 insta::assert_snapshot!(env.render_ok(r#""HEllo".match(regex-i:"h(ell)o")"#), @"HEllo");
3347 insta::assert_snapshot!(env.render_ok(r#""hEllo".match(glob:"h*o")"#), @"hEllo");
3348 insta::assert_snapshot!(env.render_ok(r#""Hello".match(glob:"h*o")"#), @"");
3349 insta::assert_snapshot!(env.render_ok(r#""HEllo".match(glob-i:"h*o")"#), @"HEllo");
3350 insta::assert_snapshot!(env.render_ok(r#""hello".match("he")"#), @"he");
3351 insta::assert_snapshot!(env.render_ok(r#""hello".match(substring:"he")"#), @"he");
3352 insta::assert_snapshot!(env.render_ok(r#""hello".match(exact:"he")"#), @"");
3353
3354 insta::assert_snapshot!(env.render_ok(r#""🥺".match(regex:'(?-u)^(?:.)')"#), @"<Error: incomplete utf-8 byte sequence from index 0>");
3358
3359 insta::assert_snapshot!(env.parse_err(r#""hello".match(false)"#), @r#"
3360 --> 1:15
3361 |
3362 1 | "hello".match(false)
3363 | ^---^
3364 |
3365 = Expected string pattern
3366 "#);
3367 insta::assert_snapshot!(env.parse_err(r#""🥺".match(not-a-pattern:"abc")"#), @r#"
3368 --> 1:11
3369 |
3370 1 | "🥺".match(not-a-pattern:"abc")
3371 | ^-----------------^
3372 |
3373 = Bad string pattern
3374 Invalid string pattern kind `not-a-pattern:`
3375 "#);
3376
3377 insta::assert_snapshot!(env.render_ok(r#""".first_line()"#), @"");
3378 insta::assert_snapshot!(env.render_ok(r#""foo\nbar".first_line()"#), @"foo");
3379
3380 insta::assert_snapshot!(env.render_ok(r#""".lines()"#), @"");
3381 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc\n".lines()"#), @"a b c");
3382
3383 insta::assert_snapshot!(env.render_ok(r#""".split(",")"#), @"");
3384 insta::assert_snapshot!(env.render_ok(r#""a,b,c".split(",")"#), @"a b c");
3385 insta::assert_snapshot!(env.render_ok(r#""a::b::c::d".split("::")"#), @"a b c d");
3386 insta::assert_snapshot!(env.render_ok(r#""a,b,c,d".split(",", 0)"#), @"");
3387 insta::assert_snapshot!(env.render_ok(r#""a,b,c,d".split(",", 2)"#), @"a b,c,d");
3388 insta::assert_snapshot!(env.render_ok(r#""a,b,c,d".split(",", 3)"#), @"a b c,d");
3389 insta::assert_snapshot!(env.render_ok(r#""a,b,c,d".split(",", 10)"#), @"a b c d");
3390 insta::assert_snapshot!(env.render_ok(r#""abc".split(",", -1)"#), @"<Error: out of range integral type conversion attempted>");
3391 insta::assert_snapshot!(env.render_ok(r#"json("a1b2c3".split(regex:'\d+'))"#), @r#"["a","b","c",""]"#);
3392 insta::assert_snapshot!(env.render_ok(r#""foo bar baz".split(regex:'\s+')"#), @"foo bar baz");
3393 insta::assert_snapshot!(env.render_ok(r#""a1b2c3d4".split(regex:'\d+', 3)"#), @"a b c3d4");
3394 insta::assert_snapshot!(env.render_ok(r#"json("hello world".split(regex-i:"WORLD"))"#), @r#"["hello ",""]"#);
3395
3396 insta::assert_snapshot!(env.render_ok("''.upper()"), @"");
3397 insta::assert_snapshot!(env.render_ok("'ABCabc 123!@#'.upper()"), @"ABCABC 123!@#");
3398 insta::assert_snapshot!(env.render_ok("''.lower()"), @"");
3399 insta::assert_snapshot!(env.render_ok("'ABCabc 123!@#'.lower()"), @"abcabc 123!@#");
3400
3401 insta::assert_snapshot!(env.render_ok(r#""".starts_with("")"#), @"true");
3402 insta::assert_snapshot!(env.render_ok(r#""everything".starts_with("")"#), @"true");
3403 insta::assert_snapshot!(env.render_ok(r#""".starts_with("foo")"#), @"false");
3404 insta::assert_snapshot!(env.render_ok(r#""foo".starts_with("foo")"#), @"true");
3405 insta::assert_snapshot!(env.render_ok(r#""foobar".starts_with("foo")"#), @"true");
3406 insta::assert_snapshot!(env.render_ok(r#""foobar".starts_with("bar")"#), @"false");
3407
3408 insta::assert_snapshot!(env.render_ok(r#""".ends_with("")"#), @"true");
3409 insta::assert_snapshot!(env.render_ok(r#""everything".ends_with("")"#), @"true");
3410 insta::assert_snapshot!(env.render_ok(r#""".ends_with("foo")"#), @"false");
3411 insta::assert_snapshot!(env.render_ok(r#""foo".ends_with("foo")"#), @"true");
3412 insta::assert_snapshot!(env.render_ok(r#""foobar".ends_with("foo")"#), @"false");
3413 insta::assert_snapshot!(env.render_ok(r#""foobar".ends_with("bar")"#), @"true");
3414
3415 insta::assert_snapshot!(env.render_ok(r#""".remove_prefix("wip: ")"#), @"");
3416 insta::assert_snapshot!(
3417 env.render_ok(r#""wip: testing".remove_prefix("wip: ")"#),
3418 @"testing");
3419
3420 insta::assert_snapshot!(
3421 env.render_ok(r#""bar@my.example.com".remove_suffix("@other.example.com")"#),
3422 @"bar@my.example.com");
3423 insta::assert_snapshot!(
3424 env.render_ok(r#""bar@other.example.com".remove_suffix("@other.example.com")"#),
3425 @"bar");
3426
3427 insta::assert_snapshot!(env.render_ok(r#"" \n \r \t \r ".trim()"#), @"");
3428 insta::assert_snapshot!(env.render_ok(r#"" \n \r foo bar \t \r ".trim()"#), @"foo bar");
3429
3430 insta::assert_snapshot!(env.render_ok(r#"" \n \r \t \r ".trim_start()"#), @"");
3431 insta::assert_snapshot!(env.render_ok(r#"" \n \r foo bar \t \r ".trim_start()"#), @"foo bar");
3432
3433 insta::assert_snapshot!(env.render_ok(r#"" \n \r \t \r ".trim_end()"#), @"");
3434 insta::assert_snapshot!(env.render_ok(r#"" \n \r foo bar \t \r ".trim_end()"#), @" foo bar");
3435
3436 insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 0)"#), @"");
3437 insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 1)"#), @"f");
3438 insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 3)"#), @"foo");
3439 insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 4)"#), @"foo");
3440 insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(2, -1)"#), @"cde");
3441 insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-3, 99)"#), @"def");
3442 insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-6, 99)"#), @"abcdef");
3443 insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-7, 1)"#), @"a");
3444
3445 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(2, -1)"#), @"c💩");
3447 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, -3)"#), @"💩");
3448 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, -4)"#), @"");
3449 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(6, -3)"#), @"💩");
3450 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(7, -3)"#), @"");
3451 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, 4)"#), @"");
3452 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, 6)"#), @"");
3453 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, 7)"#), @"💩");
3454 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-1, 7)"#), @"");
3455 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-3, 7)"#), @"");
3456 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-4, 7)"#), @"💩");
3457
3458 insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(4, 2)"#), @"");
3460 insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-2, -4)"#), @"");
3461
3462 insta::assert_snapshot!(env.render_ok(r#""hello".escape_json()"#), @r#""hello""#);
3463 insta::assert_snapshot!(env.render_ok(r#""he \n ll \n \" o".escape_json()"#), @r#""he \n ll \n \" o""#);
3464
3465 insta::assert_snapshot!(env.render_ok(r#""hello world".replace("world", "jj")"#), @"hello jj");
3467 insta::assert_snapshot!(env.render_ok(r#""hello world world".replace("world", "jj")"#), @"hello jj jj");
3468 insta::assert_snapshot!(env.render_ok(r#""hello".replace("missing", "jj")"#), @"hello");
3469
3470 insta::assert_snapshot!(env.render_ok(r#""hello world world".replace("world", "jj", 0)"#), @"hello world world");
3472 insta::assert_snapshot!(env.render_ok(r#""hello world world".replace("world", "jj", 1)"#), @"hello jj world");
3473 insta::assert_snapshot!(env.render_ok(r#""hello world world world".replace("world", "jj", 2)"#), @"hello jj jj world");
3474
3475 insta::assert_snapshot!(env.render_ok(r#""hello world world".replace("world", "jj", -1)"#), @"<Error: out of range integral type conversion attempted>");
3477 insta::assert_snapshot!(env.render_ok(r#""hello world world".replace("world", "jj", -5)"#), @"<Error: out of range integral type conversion attempted>");
3478
3479 insta::assert_snapshot!(env.render_ok(r#""hello123world456".replace(regex:'\d+', "X")"#), @"helloXworldX");
3481 insta::assert_snapshot!(env.render_ok(r#""hello123world456".replace(regex:'\d+', "X", 1)"#), @"helloXworld456");
3482
3483 insta::assert_snapshot!(env.render_ok(r#""HELLO WORLD".replace(regex-i:"(hello) +(world)", "$2 $1")"#), @"WORLD HELLO");
3485 insta::assert_snapshot!(env.render_ok(r#""abc123".replace(regex:"([a-z]+)([0-9]+)", "$2-$1")"#), @"123-abc");
3486 insta::assert_snapshot!(env.render_ok(r#""foo123bar".replace(regex:'\d+', "[$0]")"#), @"foo[123]bar");
3487
3488 insta::assert_snapshot!(env.render_ok(r#""Hello World".replace(regex-i:"hello", "hi")"#), @"hi World");
3490 insta::assert_snapshot!(env.render_ok(r#""Hello World Hello".replace(regex-i:"hello", "hi")"#), @"hi World hi");
3491 insta::assert_snapshot!(env.render_ok(r#""Hello World Hello".replace(regex-i:"hello", "hi", 1)"#), @"hi World Hello");
3492
3493 insta::assert_snapshot!(env.render_ok(r#"'hello\d+world'.replace('\d+', "X")"#), @"helloXworld");
3495 insta::assert_snapshot!(env.render_ok(r#""(foo)($1)bar".replace("$1", "$2")"#), @"(foo)()bar");
3496 insta::assert_snapshot!(env.render_ok(r#""test(abc)end".replace("(abc)", "X")"#), @"testXend");
3497
3498 insta::assert_snapshot!(env.render_ok(r#""hello world".replace("world", description.first_line())"#), @"hello description 1");
3500
3501 insta::assert_snapshot!(env.render_ok(r#""hello world".replace("world", bad_string)"#), @"<Error: Bad>");
3503 }
3504
3505 #[test]
3506 fn test_config_value_method() {
3507 let mut env = TestTemplateEnv::new();
3508 env.add_keyword("boolean", || literal(ConfigValue::from(true)));
3509 env.add_keyword("integer", || literal(ConfigValue::from(42)));
3510 env.add_keyword("string", || literal(ConfigValue::from("foo")));
3511 env.add_keyword("string_list", || {
3512 literal(ConfigValue::from_iter(["foo", "bar"]))
3513 });
3514
3515 insta::assert_snapshot!(env.render_ok("boolean"), @"true");
3516 insta::assert_snapshot!(env.render_ok("integer"), @"42");
3517 insta::assert_snapshot!(env.render_ok("string"), @r#""foo""#);
3518 insta::assert_snapshot!(env.render_ok("string_list"), @r#"["foo", "bar"]"#);
3519
3520 insta::assert_snapshot!(env.render_ok("boolean.as_boolean()"), @"true");
3521 insta::assert_snapshot!(env.render_ok("integer.as_integer()"), @"42");
3522 insta::assert_snapshot!(env.render_ok("string.as_string()"), @"foo");
3523 insta::assert_snapshot!(env.render_ok("string_list.as_string_list()"), @"foo bar");
3524
3525 insta::assert_snapshot!(
3526 env.render_ok("boolean.as_integer()"),
3527 @"<Error: invalid type: boolean `true`, expected i64>");
3528 insta::assert_snapshot!(
3529 env.render_ok("integer.as_string()"),
3530 @"<Error: invalid type: integer `42`, expected a string>");
3531 insta::assert_snapshot!(
3532 env.render_ok("string.as_string_list()"),
3533 @r#"<Error: invalid type: string "foo", expected a sequence>"#);
3534 insta::assert_snapshot!(
3535 env.render_ok("string_list.as_boolean()"),
3536 @"<Error: invalid type: sequence, expected a boolean>");
3537 }
3538
3539 #[test]
3540 fn test_signature_and_email_methods() {
3541 let mut env = TestTemplateEnv::new();
3542
3543 env.add_keyword("author", || {
3544 literal(new_signature("Test User", "test.user@example.com"))
3545 });
3546 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user@example.com>");
3547 insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Test User");
3548 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@example.com");
3549 insta::assert_snapshot!(env.render_ok("author.email().local()"), @"test.user");
3550 insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"example.com");
3551 insta::assert_snapshot!(env.render_ok("author.timestamp()"), @"1970-01-01 00:00:00.000 +00:00");
3552
3553 env.add_keyword("author", || {
3554 literal(new_signature("Another Test User", "test.user@example.com"))
3555 });
3556 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Another Test User <test.user@example.com>");
3557 insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Another Test User");
3558 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@example.com");
3559
3560 env.add_keyword("author", || {
3561 literal(new_signature("Test User", "test.user@invalid@example.com"))
3562 });
3563 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user@invalid@example.com>");
3564 insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Test User");
3565 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@invalid@example.com");
3566 insta::assert_snapshot!(env.render_ok("author.email().local()"), @"test.user");
3567 insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"invalid@example.com");
3568
3569 env.add_keyword("author", || {
3570 literal(new_signature("Test User", "test.user"))
3571 });
3572 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user>");
3573 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user");
3574 insta::assert_snapshot!(env.render_ok("author.email().local()"), @"test.user");
3575 insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"");
3576
3577 env.add_keyword("author", || {
3578 literal(new_signature("Test User", "test.user+tag@example.com"))
3579 });
3580 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user+tag@example.com>");
3581 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user+tag@example.com");
3582 insta::assert_snapshot!(env.render_ok("author.email().local()"), @"test.user+tag");
3583 insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"example.com");
3584
3585 env.add_keyword("author", || literal(new_signature("Test User", "x@y")));
3586 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <x@y>");
3587 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"x@y");
3588 insta::assert_snapshot!(env.render_ok("author.email().local()"), @"x");
3589 insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"y");
3590
3591 env.add_keyword("author", || {
3592 literal(new_signature("", "test.user@example.com"))
3593 });
3594 insta::assert_snapshot!(env.render_ok(r#"author"#), @"<test.user@example.com>");
3595 insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"");
3596 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@example.com");
3597
3598 env.add_keyword("author", || literal(new_signature("Test User", "")));
3599 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User");
3600 insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Test User");
3601 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"");
3602 insta::assert_snapshot!(env.render_ok("author.email().local()"), @"");
3603 insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"");
3604
3605 env.add_keyword("author", || literal(new_signature("", "")));
3606 insta::assert_snapshot!(env.render_ok(r#"author"#), @"");
3607 insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"");
3608 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"");
3609 }
3610
3611 #[test]
3612 fn test_size_hint_method() {
3613 let mut env = TestTemplateEnv::new();
3614
3615 env.add_keyword("unbounded", || literal((5, None)));
3616 insta::assert_snapshot!(env.render_ok(r#"unbounded.lower()"#), @"5");
3617 insta::assert_snapshot!(env.render_ok(r#"unbounded.upper()"#), @"");
3618 insta::assert_snapshot!(env.render_ok(r#"unbounded.exact()"#), @"");
3619 insta::assert_snapshot!(env.render_ok(r#"unbounded.zero()"#), @"false");
3620
3621 env.add_keyword("bounded", || literal((0, Some(10))));
3622 insta::assert_snapshot!(env.render_ok(r#"bounded.lower()"#), @"0");
3623 insta::assert_snapshot!(env.render_ok(r#"bounded.upper()"#), @"10");
3624 insta::assert_snapshot!(env.render_ok(r#"bounded.exact()"#), @"");
3625 insta::assert_snapshot!(env.render_ok(r#"bounded.zero()"#), @"false");
3626
3627 env.add_keyword("zero", || literal((0, Some(0))));
3628 insta::assert_snapshot!(env.render_ok(r#"zero.lower()"#), @"0");
3629 insta::assert_snapshot!(env.render_ok(r#"zero.upper()"#), @"0");
3630 insta::assert_snapshot!(env.render_ok(r#"zero.exact()"#), @"0");
3631 insta::assert_snapshot!(env.render_ok(r#"zero.zero()"#), @"true");
3632 }
3633
3634 #[test]
3635 fn test_timestamp_method() {
3636 let mut env = TestTemplateEnv::new();
3637 env.add_keyword("now", || literal(Timestamp::now()));
3638 env.add_keyword("t0", || literal(new_timestamp(0, 0)));
3639 env.add_keyword("t0_plus1", || literal(new_timestamp(0, 60)));
3640 env.add_keyword("tmax", || literal(new_timestamp(i64::MAX, 0)));
3641
3642 insta::assert_snapshot!(env.render_ok("tmax"),
3644 @"<Error: Out-of-range date>");
3645
3646 insta::assert_snapshot!(
3647 env.render_ok(r#"t0.format("%Y%m%d %H:%M:%S")"#),
3648 @"19700101 00:00:00");
3649
3650 insta::assert_snapshot!(env.parse_err(r#"t0.format("%_")"#), @r#"
3652 --> 1:11
3653 |
3654 1 | t0.format("%_")
3655 | ^--^
3656 |
3657 = Invalid time format
3658 "#);
3659
3660 insta::assert_snapshot!(env.parse_err(r#"t0.format(0)"#), @r"
3662 --> 1:11
3663 |
3664 1 | t0.format(0)
3665 | ^
3666 |
3667 = Expected string literal
3668 ");
3669
3670 insta::assert_snapshot!(env.parse_err(r#"t0.format("%Y" ++ "%m")"#), @r#"
3672 --> 1:11
3673 |
3674 1 | t0.format("%Y" ++ "%m")
3675 | ^----------^
3676 |
3677 = Expected string literal
3678 "#);
3679
3680 env.add_alias("time_format", r#""%Y-%m-%d""#);
3682 env.add_alias("bad_time_format", r#""%_""#);
3683 insta::assert_snapshot!(env.render_ok(r#"t0.format(time_format)"#), @"1970-01-01");
3684 insta::assert_snapshot!(env.parse_err(r#"t0.format(bad_time_format)"#), @r#"
3685 --> 1:11
3686 |
3687 1 | t0.format(bad_time_format)
3688 | ^-------------^
3689 |
3690 = In alias `bad_time_format`
3691 --> 1:1
3692 |
3693 1 | "%_"
3694 | ^--^
3695 |
3696 = Invalid time format
3697 "#);
3698
3699 insta::assert_snapshot!(env.render_ok("t0_plus1.utc()"), @"1970-01-01 00:00:00.000 +00:00");
3700
3701 assert!(!env.render_ok("now.ago()").is_empty());
3704 assert!(!env.render_ok("now.local()").is_empty());
3705
3706 insta::assert_snapshot!(env.render_ok("t0.after('1969')"), @"true");
3707 insta::assert_snapshot!(env.render_ok("t0.before('1969')"), @"false");
3708 insta::assert_snapshot!(env.render_ok("t0.after('now')"), @"false");
3709 insta::assert_snapshot!(env.render_ok("t0.before('now')"), @"true");
3710 insta::assert_snapshot!(env.parse_err("t0.before('invalid')"), @"
3711 --> 1:11
3712 |
3713 1 | t0.before('invalid')
3714 | ^-------^
3715 |
3716 = Invalid date pattern
3717 expected unsupported identifier as position 0..7
3718 ");
3719 insta::assert_snapshot!(env.parse_err("t0.before('invalid')"), @"
3720 --> 1:11
3721 |
3722 1 | t0.before('invalid')
3723 | ^-------^
3724 |
3725 = Invalid date pattern
3726 expected unsupported identifier as position 0..7
3727 ");
3728
3729 insta::assert_snapshot!(env.parse_err("t0.after(t0)"), @"
3731 --> 1:10
3732 |
3733 1 | t0.after(t0)
3734 | ^^
3735 |
3736 = Expected string literal
3737 ");
3738 insta::assert_snapshot!(env.parse_err("t0.before(t0)"), @"
3739 --> 1:11
3740 |
3741 1 | t0.before(t0)
3742 | ^^
3743 |
3744 = Expected string literal
3745 ");
3746 }
3747
3748 #[test]
3749 fn test_timestamp_range_method() {
3750 let mut env = TestTemplateEnv::new();
3751 env.add_keyword("instant", || {
3752 literal(TimestampRange {
3753 start: new_timestamp(0, 0),
3754 end: new_timestamp(0, 0),
3755 })
3756 });
3757 env.add_keyword("one_msec", || {
3758 literal(TimestampRange {
3759 start: new_timestamp(0, 0),
3760 end: new_timestamp(1, -60),
3761 })
3762 });
3763
3764 insta::assert_snapshot!(
3765 env.render_ok("instant.start().format('%Y%m%d %H:%M:%S %Z')"),
3766 @"19700101 00:00:00 +00:00");
3767 insta::assert_snapshot!(
3768 env.render_ok("one_msec.end().format('%Y%m%d %H:%M:%S %Z')"),
3769 @"19691231 23:00:00 -01:00");
3770
3771 insta::assert_snapshot!(
3772 env.render_ok("instant.duration()"), @"less than a microsecond");
3773 insta::assert_snapshot!(
3774 env.render_ok("one_msec.duration()"), @"1 millisecond");
3775 }
3776
3777 #[test]
3778 fn test_fill_function() {
3779 let mut env = TestTemplateEnv::new();
3780 env.add_color("error", crossterm::style::Color::DarkRed);
3781
3782 insta::assert_snapshot!(
3783 env.render_ok(r#"fill(20, "The quick fox jumps over the " ++
3784 label("error", "lazy") ++ " dog\n")"#),
3785 @r"
3786 The quick fox jumps
3787 over the [38;5;1mlazy[39m dog
3788 ");
3789
3790 insta::assert_snapshot!(
3792 env.render_ok(r#"fill(9, "Longlonglongword an some short words " ++
3793 label("error", "longlonglongword and short words") ++
3794 " back out\n")"#),
3795 @r"
3796 Longlonglongword
3797 an some
3798 short
3799 words
3800 [38;5;1mlonglonglongword[39m
3801 [38;5;1mand short[39m
3802 [38;5;1mwords[39m
3803 back out
3804 ");
3805
3806 insta::assert_snapshot!(
3808 env.render_ok(r#"fill(0, "The quick fox jumps over the " ++
3809 label("error", "lazy") ++ " dog\n")"#),
3810 @r"
3811 The
3812 quick
3813 fox
3814 jumps
3815 over
3816 the
3817 [38;5;1mlazy[39m
3818 dog
3819 ");
3820
3821 insta::assert_snapshot!(
3823 env.render_ok(r#"fill(-0, "The quick fox jumps over the " ++
3824 label("error", "lazy") ++ " dog\n")"#),
3825 @r"
3826 The
3827 quick
3828 fox
3829 jumps
3830 over
3831 the
3832 [38;5;1mlazy[39m
3833 dog
3834 ");
3835
3836 insta::assert_snapshot!(
3838 env.render_ok(r#"fill(-10, "The quick fox jumps over the " ++
3839 label("error", "lazy") ++ " dog\n")"#),
3840 @"[38;5;1m<Error: out of range integral type conversion attempted>[39m");
3841
3842 insta::assert_snapshot!(
3844 env.render_ok(r#""START marker to help insta\n" ++
3845 indent(" ", fill(20, "The quick fox jumps over the " ++
3846 label("error", "lazy") ++ " dog\n"))"#),
3847 @r"
3848 START marker to help insta
3849 The quick fox jumps
3850 over the [38;5;1mlazy[39m dog
3851 ");
3852
3853 insta::assert_snapshot!(
3855 env.render_ok(r#""START marker to help insta\n" ++
3856 fill(20, indent(" ", "The quick fox jumps over the " ++
3857 label("error", "lazy") ++ " dog\n"))"#),
3858 @r"
3859 START marker to help insta
3860 The quick fox
3861 jumps over the [38;5;1mlazy[39m
3862 dog
3863 ");
3864 }
3865
3866 #[test]
3867 fn test_indent_function() {
3868 let mut env = TestTemplateEnv::new();
3869 env.add_color("error", crossterm::style::Color::DarkRed);
3870 env.add_color("warning", crossterm::style::Color::DarkYellow);
3871 env.add_color("hint", crossterm::style::Color::DarkCyan);
3872
3873 assert_eq!(env.render_ok(r#"indent("__", "")"#), "");
3876 assert_eq!(env.render_ok(r#"indent("__", "\n")"#), "\n");
3877 assert_eq!(env.render_ok(r#"indent("__", "a\n\nb")"#), "__a\n\n__b");
3878
3879 insta::assert_snapshot!(
3881 env.render_ok(r#"indent("__", label("error", "a\n") ++ label("warning", "b\n"))"#),
3882 @r"
3883 [38;5;1m__a[39m
3884 [38;5;3m__b[39m
3885 ");
3886
3887 insta::assert_snapshot!(
3889 env.render_ok(r#"indent("__", label("error", "a") ++ label("warning", "b\nc"))"#),
3890 @r"
3891 [38;5;1m__a[38;5;3mb[39m
3892 [38;5;3m__c[39m
3893 ");
3894
3895 insta::assert_snapshot!(
3897 env.render_ok(r#"indent(label("error", "XX"), "a\nb\n")"#),
3898 @r"
3899 [38;5;1mXX[39ma
3900 [38;5;1mXX[39mb
3901 ");
3902
3903 insta::assert_snapshot!(
3905 env.render_ok(r#"indent(label("hint", "A"),
3906 label("warning", indent(label("hint", "B"),
3907 label("error", "x\n") ++ "y")))"#),
3908 @r"
3909 [38;5;6mAB[38;5;1mx[39m
3910 [38;5;6mAB[38;5;3my[39m
3911 ");
3912 }
3913
3914 #[test]
3915 fn test_pad_function() {
3916 let mut env = TestTemplateEnv::new();
3917 env.add_keyword("bad_string", || new_error_property::<String>("Bad"));
3918 env.add_color("red", crossterm::style::Color::Red);
3919 env.add_color("cyan", crossterm::style::Color::DarkCyan);
3920
3921 insta::assert_snapshot!(
3923 env.render_ok(r"'{' ++ pad_start(5, label('red', 'foo')) ++ '}'"),
3924 @"{ [38;5;9mfoo[39m}");
3925 insta::assert_snapshot!(
3926 env.render_ok(r"'{' ++ pad_end(5, label('red', 'foo')) ++ '}'"),
3927 @"{[38;5;9mfoo[39m }");
3928 insta::assert_snapshot!(
3929 env.render_ok(r"'{' ++ pad_centered(5, label('red', 'foo')) ++ '}'"),
3930 @"{ [38;5;9mfoo[39m }");
3931
3932 insta::assert_snapshot!(
3934 env.render_ok(r"pad_start(5, label('red', 'foo'), fill_char=label('cyan', '='))"),
3935 @"[38;5;6m==[38;5;9mfoo[39m");
3936 insta::assert_snapshot!(
3937 env.render_ok(r"pad_end(5, label('red', 'foo'), fill_char=label('cyan', '='))"),
3938 @"[38;5;9mfoo[38;5;6m==[39m");
3939 insta::assert_snapshot!(
3940 env.render_ok(r"pad_centered(5, label('red', 'foo'), fill_char=label('cyan', '='))"),
3941 @"[38;5;6m=[38;5;9mfoo[38;5;6m=[39m");
3942
3943 insta::assert_snapshot!(
3946 env.render_ok(r"pad_start(3, 'foo', fill_char=bad_string)"),
3947 @"foo");
3948 insta::assert_snapshot!(
3949 env.render_ok(r"pad_end(5, 'foo', fill_char=bad_string)"),
3950 @"foo<<Error: Error: Bad>Bad>");
3951 insta::assert_snapshot!(
3952 env.render_ok(r"pad_centered(5, 'foo', fill_char=bad_string)"),
3953 @"<Error: Bad>foo<Error: Bad>");
3954
3955 insta::assert_snapshot!(
3957 env.render_ok("pad_start(-1, 'foo')"),
3958 @"<Error: out of range integral type conversion attempted>");
3959 }
3960
3961 #[test]
3962 fn test_hash_function() {
3963 let mut env = TestTemplateEnv::new();
3964 env.add_color("red", crossterm::style::Color::Red);
3965
3966 assert_eq!(env.render_ok("hash(false)"), env.render_ok("hash('false')"));
3969 assert_eq!(env.render_ok("hash(0)"), env.render_ok("hash('0')"));
3970 assert_eq!(
3971 env.render_ok("hash(0)"),
3972 env.render_ok("hash(label('red', '0'))")
3973 );
3974 }
3975
3976 #[test]
3977 fn test_truncate_function() {
3978 let mut env = TestTemplateEnv::new();
3979 env.add_color("red", crossterm::style::Color::Red);
3980
3981 insta::assert_snapshot!(
3982 env.render_ok(r"truncate_start(2, label('red', 'foobar')) ++ 'baz'"),
3983 @"[38;5;9mar[39mbaz");
3984 insta::assert_snapshot!(
3985 env.render_ok("truncate_start(5, 'foo', 'bar')"), @"foo");
3986 insta::assert_snapshot!(
3987 env.render_ok("truncate_start(9, 'foobarbazquux', 'dotdot')"), @"dotdotuux");
3988
3989 insta::assert_snapshot!(
3990 env.render_ok(r"truncate_end(2, label('red', 'foobar')) ++ 'baz'"),
3991 @"[38;5;9mfo[39mbaz");
3992 insta::assert_snapshot!(
3993 env.render_ok("truncate_end(5, 'foo', 'bar')"), @"foo");
3994 insta::assert_snapshot!(
3995 env.render_ok("truncate_end(9, 'foobarbazquux', 'dotdot')"), @"foodotdot");
3996
3997 insta::assert_snapshot!(
3999 env.render_ok("truncate_end(-1, 'foo')"),
4000 @"<Error: out of range integral type conversion attempted>");
4001 }
4002
4003 #[test]
4004 fn test_label_function() {
4005 let mut env = TestTemplateEnv::new();
4006 env.add_keyword("empty", || literal(true));
4007 env.add_color("error", crossterm::style::Color::DarkRed);
4008 env.add_color("warning", crossterm::style::Color::DarkYellow);
4009
4010 insta::assert_snapshot!(
4012 env.render_ok(r#"label("error", "text")"#),
4013 @"[38;5;1mtext[39m");
4014
4015 insta::assert_snapshot!(
4017 env.render_ok(r#"label("error".first_line(), "text")"#),
4018 @"[38;5;1mtext[39m");
4019
4020 insta::assert_snapshot!(
4022 env.render_ok("label(fill(-1, 'foo'), 'text')"),
4023 @"[38;5;1m<Error: out of range integral type conversion attempted>[39m");
4024
4025 insta::assert_snapshot!(
4027 env.render_ok(r#"label(if(empty, "error", "warning"), "text")"#),
4028 @"[38;5;1mtext[39m");
4029 }
4030
4031 #[test]
4032 fn test_raw_escape_sequence_function_strip_labels() {
4033 let mut env = TestTemplateEnv::new();
4034 env.add_color("error", crossterm::style::Color::DarkRed);
4035 env.add_color("warning", crossterm::style::Color::DarkYellow);
4036
4037 insta::assert_snapshot!(
4038 env.render_ok(r#"raw_escape_sequence(label("error warning", "text"))"#),
4039 @"text",
4040 );
4041 }
4042
4043 #[test]
4044 fn test_raw_escape_sequence_function_ansi_escape() {
4045 let env = TestTemplateEnv::new();
4046
4047 insta::assert_snapshot!(env.render_ok(r#""\e""#), @"␛");
4049 insta::assert_snapshot!(env.render_ok(r#""\x1b""#), @"␛");
4050 insta::assert_snapshot!(env.render_ok(r#""\x1B""#), @"␛");
4051 insta::assert_snapshot!(
4052 env.render_ok(r#""]8;;"
4053 ++ "http://example.com"
4054 ++ "\e\\"
4055 ++ "Example"
4056 ++ "\x1b]8;;\x1B\\""#),
4057 @r"␛]8;;http://example.com␛\Example␛]8;;␛\");
4058
4059 insta::assert_snapshot!(env.render_ok(r#"raw_escape_sequence("\e")"#), @"");
4061 insta::assert_snapshot!(env.render_ok(r#"raw_escape_sequence("\x1b")"#), @"");
4062 insta::assert_snapshot!(env.render_ok(r#"raw_escape_sequence("\x1B")"#), @"");
4063 insta::assert_snapshot!(
4064 env.render_ok(r#"raw_escape_sequence("]8;;"
4065 ++ "http://example.com"
4066 ++ "\e\\"
4067 ++ "Example"
4068 ++ "\x1b]8;;\x1B\\")"#),
4069 @r"]8;;http://example.com\Example]8;;\");
4070 }
4071
4072 #[test]
4073 fn test_hyperlink_function_with_color() {
4074 let env = TestTemplateEnv::new();
4075 insta::assert_snapshot!(
4077 env.render_ok(r#"hyperlink("http://example.com", "Example")"#),
4078 @r"]8;;http://example.com\Example]8;;\");
4079 }
4080
4081 #[test]
4082 fn test_hyperlink_function_without_color() {
4083 let env = TestTemplateEnv::new();
4084 insta::assert_snapshot!(
4086 env.render_plain(r#"hyperlink("http://example.com", "Example")"#),
4087 @"Example");
4088 }
4089
4090 #[test]
4091 fn test_hyperlink_function_custom_fallback() {
4092 let env = TestTemplateEnv::new();
4093 insta::assert_snapshot!(
4095 env.render_plain(r#"hyperlink("http://example.com", "Example", "URL: http://example.com")"#),
4096 @"URL: http://example.com");
4097 }
4098
4099 #[test]
4100 fn test_hyperlink_function_stringify() {
4101 let env = TestTemplateEnv::new();
4102 insta::assert_snapshot!(
4104 env.render_ok(r#"stringify(hyperlink("http://example.com", "Example"))"#),
4105 @"Example");
4106 insta::assert_snapshot!(
4108 env.render_ok(r#"stringify(hyperlink("http://example.com", "Example")).upper()"#),
4109 @"EXAMPLE");
4110 }
4111
4112 #[test]
4113 fn test_hyperlink_function_with_separate() {
4114 let env = TestTemplateEnv::new();
4115 insta::assert_snapshot!(
4117 env.render_ok(r#"separate(" | ", hyperlink("http://a.com", "A"), hyperlink("http://b.com", "B"))"#),
4118 @"\u{1b}]8;;http://a.com\u{1b}\\A\u{1b}]8;;\u{1b}\\ | \u{1b}]8;;http://b.com\u{1b}\\B\u{1b}]8;;\u{1b}\\");
4119 }
4120
4121 #[test]
4122 fn test_hyperlink_function_with_coalesce() {
4123 let env = TestTemplateEnv::new();
4124 insta::assert_snapshot!(
4126 env.render_ok(r#"coalesce(hyperlink("http://example.com", "Link"), "fallback")"#),
4127 @"\u{1b}]8;;http://example.com\u{1b}\\Link\u{1b}]8;;\u{1b}\\");
4128 insta::assert_snapshot!(
4130 env.render_ok(r#"coalesce(hyperlink("http://example.com", ""), "fallback")"#),
4131 @"fallback");
4132 }
4133
4134 #[test]
4135 fn test_hyperlink_function_with_if() {
4136 let env = TestTemplateEnv::new();
4137 insta::assert_snapshot!(
4139 env.render_ok(r#"if(true, hyperlink("http://example.com", "Yes"), "No")"#),
4140 @r"]8;;http://example.com\Yes]8;;\");
4141 insta::assert_snapshot!(
4142 env.render_ok(r#"if(false, "Yes", hyperlink("http://example.com", "No"))"#),
4143 @r"]8;;http://example.com\No]8;;\");
4144 }
4145
4146 #[test]
4147 fn test_hyperlink_function_plain_with_separate() {
4148 let env = TestTemplateEnv::new();
4149 insta::assert_snapshot!(
4151 env.render_plain(r#"separate(" | ", hyperlink("http://a.com", "A"), hyperlink("http://b.com", "B"))"#),
4152 @"A | B");
4153 }
4154
4155 #[test]
4156 fn test_stringify_function() {
4157 let mut env = TestTemplateEnv::new();
4158 env.add_keyword("none_i64", || literal(None::<i64>));
4159 env.add_color("error", crossterm::style::Color::DarkRed);
4160
4161 insta::assert_snapshot!(env.render_ok("stringify(false)"), @"false");
4162 insta::assert_snapshot!(env.render_ok("stringify(42).len()"), @"2");
4163 insta::assert_snapshot!(env.render_ok("stringify(none_i64)"), @"");
4164 insta::assert_snapshot!(env.render_ok("stringify(label('error', 'text'))"), @"text");
4165 }
4166
4167 #[test]
4168 fn test_json_function() {
4169 let mut env = TestTemplateEnv::new();
4170 env.add_keyword("none_i64", || literal(None::<i64>));
4171 env.add_keyword("string_list", || {
4172 literal(vec!["foo".to_owned(), "bar".to_owned()])
4173 });
4174 env.add_keyword("config_value_table", || {
4175 literal(ConfigValue::from_iter([("foo", "bar")]))
4176 });
4177 env.add_keyword("signature", || {
4178 literal(Signature {
4179 name: "Test User".to_owned(),
4180 email: "test.user@example.com".to_owned(),
4181 timestamp: Timestamp {
4182 timestamp: MillisSinceEpoch(0),
4183 tz_offset: 0,
4184 },
4185 })
4186 });
4187 env.add_keyword("email", || literal(Email("foo@bar".to_owned())));
4188 env.add_keyword("size_hint", || literal((5, None)));
4189 env.add_keyword("timestamp", || {
4190 literal(Timestamp {
4191 timestamp: MillisSinceEpoch(0),
4192 tz_offset: 0,
4193 })
4194 });
4195 env.add_keyword("timestamp_range", || {
4196 literal(TimestampRange {
4197 start: Timestamp {
4198 timestamp: MillisSinceEpoch(0),
4199 tz_offset: 0,
4200 },
4201 end: Timestamp {
4202 timestamp: MillisSinceEpoch(86_400_000),
4203 tz_offset: -60,
4204 },
4205 })
4206 });
4207
4208 insta::assert_snapshot!(env.render_ok(r#"json('"quoted"')"#), @r#""\"quoted\"""#);
4209 insta::assert_snapshot!(env.render_ok(r#"json(string_list)"#), @r#"["foo","bar"]"#);
4210 insta::assert_snapshot!(env.render_ok("json(false)"), @"false");
4211 insta::assert_snapshot!(env.render_ok("json(42)"), @"42");
4212 insta::assert_snapshot!(env.render_ok("json(none_i64)"), @"null");
4213 insta::assert_snapshot!(env.render_ok(r#"json(config_value_table)"#), @r#"{"foo":"bar"}"#);
4214 insta::assert_snapshot!(env.render_ok("json(email)"), @r#""foo@bar""#);
4215 insta::assert_snapshot!(
4216 env.render_ok("json(signature)"),
4217 @r#"{"name":"Test User","email":"test.user@example.com","timestamp":"1970-01-01T00:00:00Z"}"#);
4218 insta::assert_snapshot!(env.render_ok("json(size_hint)"), @"[5,null]");
4219 insta::assert_snapshot!(env.render_ok("json(timestamp)"), @r#""1970-01-01T00:00:00Z""#);
4220 insta::assert_snapshot!(
4221 env.render_ok("json(timestamp_range)"),
4222 @r#"{"start":"1970-01-01T00:00:00Z","end":"1970-01-01T23:00:00-01:00"}"#);
4223
4224 insta::assert_snapshot!(env.parse_err(r#"json(string_list.map(|s| s))"#), @"
4226 --> 1:6
4227 |
4228 1 | json(string_list.map(|s| s))
4229 | ^--------------------^
4230 |
4231 = Expected expression of type `Serialize`, but actual type is `ListTemplate`
4232 ");
4233 assert_matches!(
4234 env.parse_err_kind("json(label('', ''))"),
4235 TemplateParseErrorKind::Expression(_)
4236 );
4237 }
4238
4239 #[test]
4240 fn test_coalesce_function() {
4241 let mut env = TestTemplateEnv::new();
4242 env.add_keyword("bad_string", || new_error_property::<String>("Bad"));
4243 env.add_keyword("empty_string", || literal("".to_owned()));
4244 env.add_keyword("non_empty_string", || literal("a".to_owned()));
4245
4246 insta::assert_snapshot!(env.render_ok(r#"coalesce()"#), @"");
4247 insta::assert_snapshot!(env.render_ok(r#"coalesce("")"#), @"");
4248 insta::assert_snapshot!(env.render_ok(r#"coalesce("", "a", "", "b")"#), @"a");
4249 insta::assert_snapshot!(
4250 env.render_ok(r#"coalesce(empty_string, "", non_empty_string)"#), @"a");
4251
4252 insta::assert_snapshot!(env.render_ok(r#"coalesce(false, true)"#), @"false");
4254
4255 insta::assert_snapshot!(env.render_ok(r#"coalesce(bad_string, "a")"#), @"<Error: Bad>");
4257 insta::assert_snapshot!(env.render_ok(r#"coalesce("a", bad_string)"#), @"a");
4259
4260 insta::assert_snapshot!(env.parse_err(r#"coalesce("a", value2="b")"#), @r#"
4262 --> 1:15
4263 |
4264 1 | coalesce("a", value2="b")
4265 | ^--------^
4266 |
4267 = Function `coalesce`: Unexpected keyword arguments
4268 "#);
4269 }
4270
4271 #[test]
4272 fn test_concat_function() {
4273 let mut env = TestTemplateEnv::new();
4274 env.add_keyword("empty", || literal(true));
4275 env.add_keyword("hidden", || literal(false));
4276 env.add_color("empty", crossterm::style::Color::DarkGreen);
4277 env.add_color("error", crossterm::style::Color::DarkRed);
4278 env.add_color("warning", crossterm::style::Color::DarkYellow);
4279
4280 insta::assert_snapshot!(env.render_ok(r#"concat()"#), @"");
4281 insta::assert_snapshot!(
4282 env.render_ok(r#"concat(hidden, empty)"#),
4283 @"false[38;5;2mtrue[39m");
4284 insta::assert_snapshot!(
4285 env.render_ok(r#"concat(label("error", ""), label("warning", "a"), "b")"#),
4286 @"[38;5;3ma[39mb");
4287
4288 insta::assert_snapshot!(env.parse_err(r#"concat("a", value2="b")"#), @r#"
4290 --> 1:13
4291 |
4292 1 | concat("a", value2="b")
4293 | ^--------^
4294 |
4295 = Function `concat`: Unexpected keyword arguments
4296 "#);
4297 }
4298
4299 #[test]
4300 fn test_join_function() {
4301 let mut env = TestTemplateEnv::new();
4302 env.add_keyword("description", || literal("".to_owned()));
4303 env.add_keyword("empty", || literal(true));
4304 env.add_keyword("hidden", || literal(false));
4305 env.add_color("empty", crossterm::style::Color::DarkGreen);
4306 env.add_color("error", crossterm::style::Color::DarkRed);
4307 env.add_color("warning", crossterm::style::Color::DarkYellow);
4308
4309 insta::assert_snapshot!(env.render_ok(r#"join(",")"#), @"");
4311 insta::assert_snapshot!(env.render_ok(r#"join(",", "")"#), @"");
4312 insta::assert_snapshot!(env.render_ok(r#"join(",", "a")"#), @"a");
4313 insta::assert_snapshot!(env.render_ok(r#"join(",", "a", "b")"#), @"a,b");
4314 insta::assert_snapshot!(env.render_ok(r#"join(",", "a", "", "b")"#), @"a,,b");
4315 insta::assert_snapshot!(env.render_ok(r#"join(",", "a", "b", "")"#), @"a,b,");
4316 insta::assert_snapshot!(env.render_ok(r#"join(",", "", "a", "b")"#), @",a,b");
4317 insta::assert_snapshot!(
4318 env.render_ok(r#"join("--", 1, "", true, "test", "")"#),
4319 @"1----true--test--");
4320
4321 insta::assert_snapshot!(env.parse_err(r#"join()"#), @r"
4323 --> 1:6
4324 |
4325 1 | join()
4326 | ^
4327 |
4328 = Function `join`: Expected at least 1 arguments
4329 ");
4330
4331 insta::assert_snapshot!(
4333 env.render_ok(r#"join(",", label("error", ""), label("warning", "a"), "b")"#),
4334 @",[38;5;3ma[39m,b");
4335 insta::assert_snapshot!(
4336 env.render_ok(
4337 r#"join(label("empty", "<>"), label("error", "a"), label("warning", ""), "b")"#),
4338 @"[38;5;1ma[38;5;2m<><>[39mb");
4339
4340 insta::assert_snapshot!(env.render_ok(r#"join(",", "a", ("" ++ ""))"#), @"a,");
4342 insta::assert_snapshot!(env.render_ok(r#"join(",", "a", ("" ++ "b"))"#), @"a,b");
4343
4344 insta::assert_snapshot!(
4346 env.render_ok(r#"join(",", "a", join("|", "", ""))"#), @"a,|");
4347 insta::assert_snapshot!(
4348 env.render_ok(r#"join(",", "a", join("|", "b", ""))"#), @"a,b|");
4349 insta::assert_snapshot!(
4350 env.render_ok(r#"join(",", "a", join("|", "b", "c"))"#), @"a,b|c");
4351
4352 insta::assert_snapshot!(
4354 env.render_ok(r#"join(",", hidden, description, empty)"#),
4355 @"false,,[38;5;2mtrue[39m");
4356 insta::assert_snapshot!(
4357 env.render_ok(r#"join(hidden, "X", "Y", "Z")"#),
4358 @"XfalseYfalseZ");
4359 insta::assert_snapshot!(
4360 env.render_ok(r#"join(hidden, empty)"#),
4361 @"[38;5;2mtrue[39m");
4362
4363 insta::assert_snapshot!(env.parse_err(r#"join(",", "a", arg="b")"#), @r#"
4365 --> 1:16
4366 |
4367 1 | join(",", "a", arg="b")
4368 | ^-----^
4369 |
4370 = Function `join`: Unexpected keyword arguments
4371 "#);
4372
4373 env.add_keyword("str_list", || {
4375 literal(vec!["foo".to_owned(), "bar".to_owned()])
4376 });
4377 env.add_keyword("none_int", || literal(None::<i64>));
4378 env.add_keyword("some_int", || literal(Some(67)));
4379 env.add_keyword("cfg_val", || {
4380 literal(ConfigValue::from_iter([("foo", "bar")]))
4381 });
4382 env.add_keyword("email", || literal(Email("me@example.com".to_owned())));
4383 env.add_keyword("signature", || {
4384 literal(new_signature("User", "user@example.com"))
4385 });
4386 env.add_keyword("size_hint", || literal((10, None)));
4387 env.add_keyword("timestamp", || literal(new_timestamp(0, 0)));
4388 env.add_keyword("timestamp_range", || {
4389 literal(TimestampRange {
4390 start: new_timestamp(0, 0),
4391 end: new_timestamp(0, 0),
4392 })
4393 });
4394 insta::assert_snapshot!(
4395 env.render_ok("join('|', str_list, 42, none_int, some_int)"),
4396 @"foo bar|42||67");
4397 insta::assert_snapshot!(
4398 env.render_ok("join('|', cfg_val, email, signature)"),
4399 @r#"{ foo = "bar" }|me@example.com|User <user@example.com>"#);
4400 insta::assert_snapshot!(
4401 env.render_ok("join('|', timestamp, timestamp_range, str_list.map(|x| x))"),
4402 @"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");
4403 assert_matches!(
4404 env.parse_err_kind("join('|', size_hint)"),
4405 TemplateParseErrorKind::Expression(_)
4406 );
4407 }
4408
4409 #[test]
4410 fn test_separate_function() {
4411 let mut env = TestTemplateEnv::new();
4412 env.add_keyword("description", || literal("".to_owned()));
4413 env.add_keyword("empty", || literal(true));
4414 env.add_keyword("hidden", || literal(false));
4415 env.add_color("empty", crossterm::style::Color::DarkGreen);
4416 env.add_color("error", crossterm::style::Color::DarkRed);
4417 env.add_color("warning", crossterm::style::Color::DarkYellow);
4418
4419 insta::assert_snapshot!(env.render_ok(r#"separate(" ")"#), @"");
4420 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "")"#), @"");
4421 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a")"#), @"a");
4422 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", "b")"#), @"a b");
4423 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", "", "b")"#), @"a b");
4424 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", "b", "")"#), @"a b");
4425 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "", "a", "b")"#), @"a b");
4426
4427 insta::assert_snapshot!(
4429 env.render_ok(r#"separate(" ", label("error", ""), label("warning", "a"), "b")"#),
4430 @"[38;5;3ma[39m b");
4431
4432 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", ("" ++ ""))"#), @"a");
4434 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", ("" ++ "b"))"#), @"a b");
4435
4436 insta::assert_snapshot!(
4438 env.render_ok(r#"separate(" ", "a", separate("|", "", ""))"#), @"a");
4439 insta::assert_snapshot!(
4440 env.render_ok(r#"separate(" ", "a", separate("|", "b", ""))"#), @"a b");
4441 insta::assert_snapshot!(
4442 env.render_ok(r#"separate(" ", "a", separate("|", "b", "c"))"#), @"a b|c");
4443
4444 insta::assert_snapshot!(
4446 env.render_ok(r#"separate(" ", "a", if(true, ""))"#), @"a");
4447 insta::assert_snapshot!(
4448 env.render_ok(r#"separate(" ", "a", if(true, "", "f"))"#), @"a");
4449 insta::assert_snapshot!(
4450 env.render_ok(r#"separate(" ", "a", if(false, "t"))"#), @"a");
4451 insta::assert_snapshot!(
4452 env.render_ok(r#"separate(" ", "a", if(false, "t", ""))"#), @"a");
4453 insta::assert_snapshot!(
4454 env.render_ok(r#"separate(" ", "a", if(true, "t", "f"))"#), @"a t");
4455
4456 insta::assert_snapshot!(
4458 env.render_ok(r#"separate(" ", hidden, description, empty)"#),
4459 @"false [38;5;2mtrue[39m");
4460
4461 insta::assert_snapshot!(
4463 env.render_ok(r#"separate(hidden, "X", "Y", "Z")"#),
4464 @"XfalseYfalseZ");
4465
4466 insta::assert_snapshot!(env.parse_err(r#"separate(" ", "a", value2="b")"#), @r#"
4468 --> 1:20
4469 |
4470 1 | separate(" ", "a", value2="b")
4471 | ^--------^
4472 |
4473 = Function `separate`: Unexpected keyword arguments
4474 "#);
4475 }
4476
4477 #[test]
4478 fn test_surround_function() {
4479 let mut env = TestTemplateEnv::new();
4480 env.add_keyword("lt", || literal("<".to_owned()));
4481 env.add_keyword("gt", || literal(">".to_owned()));
4482 env.add_keyword("content", || literal("content".to_owned()));
4483 env.add_keyword("empty_content", || literal("".to_owned()));
4484 env.add_color("error", crossterm::style::Color::DarkRed);
4485 env.add_color("paren", crossterm::style::Color::Cyan);
4486
4487 insta::assert_snapshot!(env.render_ok(r#"surround("{", "}", "")"#), @"");
4488 insta::assert_snapshot!(env.render_ok(r#"surround("{", "}", "a")"#), @"{a}");
4489
4490 insta::assert_snapshot!(
4492 env.render_ok(
4493 r#"surround(label("paren", "("), label("paren", ")"), label("error", "a"))"#),
4494 @"[38;5;14m([38;5;1ma[38;5;14m)[39m");
4495
4496 insta::assert_snapshot!(
4498 env.render_ok(r#"surround(lt, gt, content)"#),
4499 @"<content>");
4500 insta::assert_snapshot!(
4501 env.render_ok(r#"surround(lt, gt, empty_content)"#),
4502 @"");
4503
4504 insta::assert_snapshot!(
4506 env.render_ok(r#"surround(lt, gt, if(empty_content, "", "empty"))"#),
4507 @"<empty>");
4508 insta::assert_snapshot!(
4509 env.render_ok(r#"surround(lt, gt, if(empty_content, "not empty", ""))"#),
4510 @"");
4511 }
4512
4513 #[test]
4514 fn test_config_function() {
4515 use jj_lib::config::ConfigLayer;
4516 use jj_lib::config::ConfigSource;
4517
4518 let mut config = StackedConfig::with_defaults();
4519 config
4520 .add_layer(ConfigLayer::parse(ConfigSource::User, "user.name = 'Test User'").unwrap());
4521 config.add_layer(
4522 ConfigLayer::parse(ConfigSource::User, "user.email = 'test@example.com'").unwrap(),
4523 );
4524
4525 let env = TestTemplateEnv::with_config(config);
4526
4527 insta::assert_snapshot!(env.render_ok(r#"config("user.name")"#), @"'Test User'");
4529 insta::assert_snapshot!(env.render_ok(r#"config("user.email")"#), @"'test@example.com'");
4530 insta::assert_snapshot!(env.render_ok(r#"config("user")"#), @"{ email = 'test@example.com', name = 'Test User' }");
4531
4532 insta::assert_snapshot!(env.render_ok(r#"config("non.existent")"#), @"");
4534
4535 insta::assert_snapshot!(env.render_ok(r#"if(config("user.name"), "yes", "no")"#), @"yes");
4537 insta::assert_snapshot!(env.render_ok(r#"if(config("non.existent"), "yes", "no")"#), @"no");
4538
4539 insta::assert_snapshot!(env.parse_err("config('user|name')"), @"
4541 --> 1:8
4542 |
4543 1 | config('user|name')
4544 | ^---------^
4545 |
4546 = Failed to parse config name
4547 TOML parse error at line 1, column 5
4548 |
4549 1 | user|name
4550 | ^
4551 invalid unquoted key, expected letters, numbers, `-`, `_`
4552 ");
4553 }
4554}