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