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_exact_arguments()?;
1084 let start_idx_property =
1085 expect_isize_expression(language, diagnostics, build_ctx, start_idx)?;
1086 let end_idx_property =
1087 expect_isize_expression(language, diagnostics, build_ctx, end_idx)?;
1088 let out_property = (self_property, start_idx_property, end_idx_property).map(
1089 |(s, start_idx, end_idx)| {
1090 let start_idx = string_index_to_char_boundary(&s, start_idx);
1091 let end_idx = string_index_to_char_boundary(&s, end_idx);
1092 s.get(start_idx..end_idx).unwrap_or_default().to_owned()
1093 },
1094 );
1095 Ok(out_property.into_dyn_wrapped())
1096 },
1097 );
1098 map.insert(
1099 "first_line",
1100 |_language, _diagnostics, _build_ctx, self_property, function| {
1101 function.expect_no_arguments()?;
1102 let out_property =
1103 self_property.map(|s| s.lines().next().unwrap_or_default().to_string());
1104 Ok(out_property.into_dyn_wrapped())
1105 },
1106 );
1107 map.insert(
1108 "lines",
1109 |_language, _diagnostics, _build_ctx, self_property, function| {
1110 function.expect_no_arguments()?;
1111 let out_property = self_property.map(|s| s.lines().map(|l| l.to_owned()).collect_vec());
1112 Ok(out_property.into_dyn_wrapped())
1113 },
1114 );
1115 map.insert(
1116 "split",
1117 |language, diagnostics, build_ctx, self_property, function| {
1118 let ([separator_node], [limit_node]) = function.expect_arguments()?;
1119 let pattern = template_parser::expect_string_pattern(separator_node)?;
1120 let regex = pattern.to_regex();
1121
1122 if let Some(limit_node) = limit_node {
1123 let limit_property =
1124 expect_usize_expression(language, diagnostics, build_ctx, limit_node)?;
1125 let out_property =
1126 (self_property, limit_property).and_then(move |(haystack, limit)| {
1127 let haystack_bytes = haystack.as_bytes();
1128 let parts: Vec<_> = regex
1129 .splitn(haystack_bytes, limit)
1130 .map(|part| str::from_utf8(part).map(|s| s.to_owned()))
1131 .try_collect()?;
1132 Ok(parts)
1133 });
1134 Ok(out_property.into_dyn_wrapped())
1135 } else {
1136 let out_property = self_property.and_then(move |haystack| {
1137 let haystack_bytes = haystack.as_bytes();
1138 let parts: Vec<_> = regex
1139 .split(haystack_bytes)
1140 .map(|part| str::from_utf8(part).map(|s| s.to_owned()))
1141 .try_collect()?;
1142 Ok(parts)
1143 });
1144 Ok(out_property.into_dyn_wrapped())
1145 }
1146 },
1147 );
1148 map.insert(
1149 "upper",
1150 |_language, _diagnostics, _build_ctx, self_property, function| {
1151 function.expect_no_arguments()?;
1152 let out_property = self_property.map(|s| s.to_uppercase());
1153 Ok(out_property.into_dyn_wrapped())
1154 },
1155 );
1156 map.insert(
1157 "lower",
1158 |_language, _diagnostics, _build_ctx, self_property, function| {
1159 function.expect_no_arguments()?;
1160 let out_property = self_property.map(|s| s.to_lowercase());
1161 Ok(out_property.into_dyn_wrapped())
1162 },
1163 );
1164 map.insert(
1165 "escape_json",
1166 |_language, _diagnostics, _build_ctx, self_property, function| {
1167 function.expect_no_arguments()?;
1168 let out_property = self_property.map(|s| serde_json::to_string(&s).unwrap());
1169 Ok(out_property.into_dyn_wrapped())
1170 },
1171 );
1172 map.insert(
1173 "replace",
1174 |language, diagnostics, build_ctx, self_property, function| {
1175 let ([pattern_node, replacement_node], [limit_node]) = function.expect_arguments()?;
1176 let pattern = template_parser::expect_string_pattern(pattern_node)?;
1177 let replacement_property =
1178 expect_stringify_expression(language, diagnostics, build_ctx, replacement_node)?;
1179
1180 let regex = pattern.to_regex();
1181
1182 if let Some(limit_node) = limit_node {
1183 let limit_property =
1184 expect_usize_expression(language, diagnostics, build_ctx, limit_node)?;
1185 let out_property = (self_property, replacement_property, limit_property).and_then(
1186 move |(haystack, replacement, limit)| {
1187 if limit == 0 {
1188 Ok(haystack)
1192 } else {
1193 let haystack_bytes = haystack.as_bytes();
1194 let replace_bytes = replacement.as_bytes();
1195 let result = regex.replacen(haystack_bytes, limit, replace_bytes);
1196 Ok(str::from_utf8(&result)?.to_owned())
1197 }
1198 },
1199 );
1200 Ok(out_property.into_dyn_wrapped())
1201 } else {
1202 let out_property = (self_property, replacement_property).and_then(
1203 move |(haystack, replacement)| {
1204 let haystack_bytes = haystack.as_bytes();
1205 let replace_bytes = replacement.as_bytes();
1206 let result = regex.replace_all(haystack_bytes, replace_bytes);
1207 Ok(str::from_utf8(&result)?.to_owned())
1208 },
1209 );
1210 Ok(out_property.into_dyn_wrapped())
1211 }
1212 },
1213 );
1214 map
1215}
1216
1217fn string_index_to_char_boundary(s: &str, i: isize) -> usize {
1222 let magnitude = i.unsigned_abs();
1224 if i < 0 {
1225 let p = s.len().saturating_sub(magnitude);
1226 (p..=s.len()).find(|&p| s.is_char_boundary(p)).unwrap()
1227 } else {
1228 let p = magnitude.min(s.len());
1229 (0..=p).rev().find(|&p| s.is_char_boundary(p)).unwrap()
1230 }
1231}
1232
1233fn builtin_config_value_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1234-> TemplateBuildMethodFnMap<'a, L, ConfigValue> {
1235 fn extract<'de, T: Deserialize<'de>>(value: ConfigValue) -> Result<T, TemplatePropertyError> {
1236 T::deserialize(value.into_deserializer())
1237 .map_err(|err| TemplatePropertyError(err.message().into()))
1239 }
1240
1241 let mut map = TemplateBuildMethodFnMap::<L, ConfigValue>::new();
1244 map.insert(
1249 "as_boolean",
1250 |_language, _diagnostics, _build_ctx, self_property, function| {
1251 function.expect_no_arguments()?;
1252 let out_property = self_property.and_then(extract::<bool>);
1253 Ok(out_property.into_dyn_wrapped())
1254 },
1255 );
1256 map.insert(
1257 "as_integer",
1258 |_language, _diagnostics, _build_ctx, self_property, function| {
1259 function.expect_no_arguments()?;
1260 let out_property = self_property.and_then(extract::<i64>);
1261 Ok(out_property.into_dyn_wrapped())
1262 },
1263 );
1264 map.insert(
1265 "as_string",
1266 |_language, _diagnostics, _build_ctx, self_property, function| {
1267 function.expect_no_arguments()?;
1268 let out_property = self_property.and_then(extract::<String>);
1269 Ok(out_property.into_dyn_wrapped())
1270 },
1271 );
1272 map.insert(
1273 "as_string_list",
1274 |_language, _diagnostics, _build_ctx, self_property, function| {
1275 function.expect_no_arguments()?;
1276 let out_property = self_property.and_then(extract::<Vec<String>>);
1277 Ok(out_property.into_dyn_wrapped())
1278 },
1279 );
1280 map
1283}
1284
1285fn builtin_signature_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1286-> TemplateBuildMethodFnMap<'a, L, Signature> {
1287 let mut map = TemplateBuildMethodFnMap::<L, Signature>::new();
1290 map.insert(
1291 "name",
1292 |_language, _diagnostics, _build_ctx, self_property, function| {
1293 function.expect_no_arguments()?;
1294 let out_property = self_property.map(|signature| signature.name);
1295 Ok(out_property.into_dyn_wrapped())
1296 },
1297 );
1298 map.insert(
1299 "email",
1300 |_language, _diagnostics, _build_ctx, self_property, function| {
1301 function.expect_no_arguments()?;
1302 let out_property = self_property.map(|signature| Email(signature.email));
1303 Ok(out_property.into_dyn_wrapped())
1304 },
1305 );
1306 map.insert(
1307 "timestamp",
1308 |_language, _diagnostics, _build_ctx, self_property, function| {
1309 function.expect_no_arguments()?;
1310 let out_property = self_property.map(|signature| signature.timestamp);
1311 Ok(out_property.into_dyn_wrapped())
1312 },
1313 );
1314 map
1315}
1316
1317fn builtin_email_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1318-> TemplateBuildMethodFnMap<'a, L, Email> {
1319 let mut map = TemplateBuildMethodFnMap::<L, Email>::new();
1322 map.insert(
1323 "local",
1324 |_language, _diagnostics, _build_ctx, self_property, function| {
1325 function.expect_no_arguments()?;
1326 let out_property = self_property.map(|email| {
1327 let (local, _) = text_util::split_email(&email.0);
1328 local.to_owned()
1329 });
1330 Ok(out_property.into_dyn_wrapped())
1331 },
1332 );
1333 map.insert(
1334 "domain",
1335 |_language, _diagnostics, _build_ctx, self_property, function| {
1336 function.expect_no_arguments()?;
1337 let out_property = self_property.map(|email| {
1338 let (_, domain) = text_util::split_email(&email.0);
1339 domain.unwrap_or_default().to_owned()
1340 });
1341 Ok(out_property.into_dyn_wrapped())
1342 },
1343 );
1344 map
1345}
1346
1347fn builtin_size_hint_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1348-> TemplateBuildMethodFnMap<'a, L, SizeHint> {
1349 let mut map = TemplateBuildMethodFnMap::<L, SizeHint>::new();
1352 map.insert(
1353 "lower",
1354 |_language, _diagnostics, _build_ctx, self_property, function| {
1355 function.expect_no_arguments()?;
1356 let out_property = self_property.and_then(|(lower, _)| Ok(i64::try_from(lower)?));
1357 Ok(out_property.into_dyn_wrapped())
1358 },
1359 );
1360 map.insert(
1361 "upper",
1362 |_language, _diagnostics, _build_ctx, self_property, function| {
1363 function.expect_no_arguments()?;
1364 let out_property =
1365 self_property.and_then(|(_, upper)| Ok(upper.map(i64::try_from).transpose()?));
1366 Ok(out_property.into_dyn_wrapped())
1367 },
1368 );
1369 map.insert(
1370 "exact",
1371 |_language, _diagnostics, _build_ctx, self_property, function| {
1372 function.expect_no_arguments()?;
1373 let out_property = self_property.and_then(|(lower, upper)| {
1374 let exact = (Some(lower) == upper).then_some(lower);
1375 Ok(exact.map(i64::try_from).transpose()?)
1376 });
1377 Ok(out_property.into_dyn_wrapped())
1378 },
1379 );
1380 map.insert(
1381 "zero",
1382 |_language, _diagnostics, _build_ctx, self_property, function| {
1383 function.expect_no_arguments()?;
1384 let out_property = self_property.map(|(_, upper)| upper == Some(0));
1385 Ok(out_property.into_dyn_wrapped())
1386 },
1387 );
1388 map
1389}
1390
1391fn builtin_timestamp_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1392-> TemplateBuildMethodFnMap<'a, L, Timestamp> {
1393 let mut map = TemplateBuildMethodFnMap::<L, Timestamp>::new();
1396 map.insert(
1397 "ago",
1398 |_language, _diagnostics, _build_ctx, self_property, function| {
1399 function.expect_no_arguments()?;
1400 let now = Timestamp::now();
1401 let format = timeago::Formatter::new();
1402 let out_property = self_property.and_then(move |timestamp| {
1403 Ok(time_util::format_duration(×tamp, &now, &format)?)
1404 });
1405 Ok(out_property.into_dyn_wrapped())
1406 },
1407 );
1408 map.insert(
1409 "format",
1410 |_language, diagnostics, _build_ctx, self_property, function| {
1411 let [format_node] = function.expect_exact_arguments()?;
1413 let format =
1414 template_parser::catch_aliases(diagnostics, format_node, |_diagnostics, node| {
1415 let format = template_parser::expect_string_literal(node)?;
1416 time_util::FormattingItems::parse(format).ok_or_else(|| {
1417 TemplateParseError::expression("Invalid time format", node.span)
1418 })
1419 })?
1420 .into_owned();
1421 let out_property = self_property.and_then(move |timestamp| {
1422 Ok(time_util::format_absolute_timestamp_with(
1423 ×tamp, &format,
1424 )?)
1425 });
1426 Ok(out_property.into_dyn_wrapped())
1427 },
1428 );
1429 map.insert(
1430 "utc",
1431 |_language, _diagnostics, _build_ctx, self_property, function| {
1432 function.expect_no_arguments()?;
1433 let out_property = self_property.map(|mut timestamp| {
1434 timestamp.tz_offset = 0;
1435 timestamp
1436 });
1437 Ok(out_property.into_dyn_wrapped())
1438 },
1439 );
1440 map.insert(
1441 "local",
1442 |_language, _diagnostics, _build_ctx, self_property, function| {
1443 function.expect_no_arguments()?;
1444 let tz_offset = std::env::var("JJ_TZ_OFFSET_MINS")
1445 .ok()
1446 .and_then(|tz_string| tz_string.parse::<i32>().ok())
1447 .unwrap_or_else(|| chrono::Local::now().offset().local_minus_utc() / 60);
1448 let out_property = self_property.map(move |mut timestamp| {
1449 timestamp.tz_offset = tz_offset;
1450 timestamp
1451 });
1452 Ok(out_property.into_dyn_wrapped())
1453 },
1454 );
1455 map.insert(
1456 "after",
1457 |_language, diagnostics, _build_ctx, self_property, function| {
1458 let [date_pattern_node] = function.expect_exact_arguments()?;
1459 let now = chrono::Local::now();
1460 let date_pattern = template_parser::catch_aliases(
1461 diagnostics,
1462 date_pattern_node,
1463 |_diagnostics, node| {
1464 let date_pattern = template_parser::expect_string_literal(node)?;
1465 DatePattern::from_str_kind(date_pattern, function.name, now).map_err(|err| {
1466 TemplateParseError::expression("Invalid date pattern", node.span)
1467 .with_source(err)
1468 })
1469 },
1470 )?;
1471 let out_property = self_property.map(move |timestamp| date_pattern.matches(×tamp));
1472 Ok(out_property.into_dyn_wrapped())
1473 },
1474 );
1475 map.insert("before", map["after"]);
1476 map.insert(
1477 "since",
1478 |language, diagnostics, build_ctx, self_property, function| {
1479 let [date_node] = function.expect_exact_arguments()?;
1480 let date_property =
1481 expect_timestamp_expression(language, diagnostics, build_ctx, date_node)?;
1482 let out_property =
1483 (self_property, date_property).and_then(move |(self_timestamp, arg_timestamp)| {
1484 Ok(TimestampRange {
1485 start: arg_timestamp,
1486 end: self_timestamp,
1487 })
1488 });
1489 Ok(out_property.into_dyn_wrapped())
1490 },
1491 );
1492 map
1493}
1494
1495fn builtin_timestamp_range_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
1496-> TemplateBuildMethodFnMap<'a, L, TimestampRange> {
1497 let mut map = TemplateBuildMethodFnMap::<L, TimestampRange>::new();
1500 map.insert(
1501 "start",
1502 |_language, _diagnostics, _build_ctx, self_property, function| {
1503 function.expect_no_arguments()?;
1504 let out_property = self_property.map(|time_range| time_range.start);
1505 Ok(out_property.into_dyn_wrapped())
1506 },
1507 );
1508 map.insert(
1509 "end",
1510 |_language, _diagnostics, _build_ctx, self_property, function| {
1511 function.expect_no_arguments()?;
1512 let out_property = self_property.map(|time_range| time_range.end);
1513 Ok(out_property.into_dyn_wrapped())
1514 },
1515 );
1516 map.insert(
1517 "duration",
1518 |_language, _diagnostics, _build_ctx, self_property, function| {
1519 function.expect_no_arguments()?;
1520 let out_property = self_property.and_then(|time_range| {
1522 let mut f = timeago::Formatter::new();
1523 f.min_unit(timeago::TimeUnit::Microseconds).ago("");
1524 let duration = time_util::format_duration(&time_range.start, &time_range.end, &f)?;
1525 if duration == "now" {
1526 Ok("less than a microsecond".to_owned())
1527 } else {
1528 Ok(duration)
1529 }
1530 });
1531 Ok(out_property.into_dyn_wrapped())
1532 },
1533 );
1534 map
1535}
1536
1537fn builtin_any_list_methods<'a, L: TemplateLanguage<'a> + ?Sized>() -> BuildAnyMethodFnMap<'a, L> {
1538 let mut map = BuildAnyMethodFnMap::<L>::new();
1541 map.insert(
1542 "join",
1543 |language, diagnostics, build_ctx, self_template, function| {
1544 let [separator_node] = function.expect_exact_arguments()?;
1545 let separator =
1546 expect_template_expression(language, diagnostics, build_ctx, separator_node)?;
1547 Ok(L::Property::wrap_template(
1548 self_template.try_join(separator).ok_or_else(|| {
1549 TemplateParseError::expected_type("Template", "AnyList", function.name_span)
1552 })?,
1553 ))
1554 },
1555 );
1556 map
1557}
1558
1559pub fn builtin_formattable_list_methods<'a, L, O>() -> TemplateBuildMethodFnMap<'a, L, Vec<O>>
1561where
1562 L: TemplateLanguage<'a> + ?Sized,
1563 L::Property: WrapTemplateProperty<'a, O> + WrapTemplateProperty<'a, Vec<O>>,
1564 O: Template + Clone + 'a,
1565{
1566 let mut map = builtin_unformattable_list_methods::<L, O>();
1567 map.insert(
1568 "join",
1569 |language, diagnostics, build_ctx, self_property, function| {
1570 let [separator_node] = function.expect_exact_arguments()?;
1571 let separator =
1572 expect_template_expression(language, diagnostics, build_ctx, separator_node)?;
1573 let template =
1574 ListPropertyTemplate::new(self_property, separator, |formatter, item| {
1575 item.format(formatter)
1576 });
1577 Ok(L::Property::wrap_template(Box::new(template)))
1578 },
1579 );
1580 map
1581}
1582
1583pub fn builtin_unformattable_list_methods<'a, L, O>() -> TemplateBuildMethodFnMap<'a, L, Vec<O>>
1585where
1586 L: TemplateLanguage<'a> + ?Sized,
1587 L::Property: WrapTemplateProperty<'a, O> + WrapTemplateProperty<'a, Vec<O>>,
1588 O: Clone + 'a,
1589{
1590 let mut map = TemplateBuildMethodFnMap::<L, Vec<O>>::new();
1593 map.insert(
1594 "len",
1595 |_language, _diagnostics, _build_ctx, self_property, function| {
1596 function.expect_no_arguments()?;
1597 let out_property = self_property.and_then(|items| Ok(i64::try_from(items.len())?));
1598 Ok(out_property.into_dyn_wrapped())
1599 },
1600 );
1601 map.insert(
1602 "filter",
1603 |language, diagnostics, build_ctx, self_property, function| {
1604 let out_property: BoxedTemplateProperty<'a, Vec<O>> =
1605 build_filter_operation(language, diagnostics, build_ctx, self_property, function)?;
1606 Ok(L::Property::wrap_property(out_property))
1607 },
1608 );
1609 map.insert(
1610 "map",
1611 |language, diagnostics, build_ctx, self_property, function| {
1612 let map_result =
1613 build_map_operation(language, diagnostics, build_ctx, self_property, function)?;
1614 Ok(L::Property::wrap_any_list(map_result))
1615 },
1616 );
1617 map.insert(
1618 "any",
1619 |language, diagnostics, build_ctx, self_property, function| {
1620 let out_property =
1621 build_any_operation(language, diagnostics, build_ctx, self_property, function)?;
1622 Ok(out_property.into_dyn_wrapped())
1623 },
1624 );
1625 map.insert(
1626 "all",
1627 |language, diagnostics, build_ctx, self_property, function| {
1628 let out_property =
1629 build_all_operation(language, diagnostics, build_ctx, self_property, function)?;
1630 Ok(out_property.into_dyn_wrapped())
1631 },
1632 );
1633 map.insert(
1634 "first",
1635 |_language, _diagnostics, _build_ctx, self_property, function| {
1636 function.expect_no_arguments()?;
1637 let out_property = self_property.and_then(|items| {
1639 items
1640 .into_iter()
1641 .next()
1642 .ok_or_else(|| TemplatePropertyError("List is empty".into()))
1643 });
1644 Ok(L::Property::wrap_property(out_property.into_dyn()))
1645 },
1646 );
1647 map.insert(
1648 "last",
1649 |_language, _diagnostics, _build_ctx, self_property, function| {
1650 function.expect_no_arguments()?;
1651 let out_property = self_property.and_then(|mut items| {
1653 items
1654 .pop()
1655 .ok_or_else(|| TemplatePropertyError("List is empty".into()))
1656 });
1657 Ok(L::Property::wrap_property(out_property.into_dyn()))
1658 },
1659 );
1660 map.insert(
1661 "get",
1662 |language, diagnostics, build_ctx, self_property, function| {
1663 let [index_node] = function.expect_exact_arguments()?;
1664 let index = expect_usize_expression(language, diagnostics, build_ctx, index_node)?;
1665 let out_property = (self_property, index).and_then(|(mut items, index)| {
1667 if index < items.len() {
1668 Ok(items.remove(index))
1669 } else {
1670 Err(TemplatePropertyError(
1671 format!("Index {index} out of bounds").into(),
1672 ))
1673 }
1674 });
1675 Ok(L::Property::wrap_property(out_property.into_dyn()))
1676 },
1677 );
1678 map.insert(
1679 "reverse",
1680 |_language, _diagnostics, _build_ctx, self_property, function| {
1681 function.expect_no_arguments()?;
1682 let out_property = self_property.map(|mut items| {
1683 items.reverse();
1684 items
1685 });
1686 Ok(L::Property::wrap_property(out_property.into_dyn()))
1687 },
1688 );
1689 map.insert(
1690 "skip",
1691 |language, diagnostics, build_ctx, self_property, function| {
1692 let [count_node] = function.expect_exact_arguments()?;
1693 let count = expect_usize_expression(language, diagnostics, build_ctx, count_node)?;
1694 let out_property = (self_property, count)
1695 .map(|(items, count)| items.into_iter().skip(count).collect_vec());
1696 Ok(L::Property::wrap_property(out_property.into_dyn()))
1697 },
1698 );
1699 map.insert(
1700 "take",
1701 |language, diagnostics, build_ctx, self_property, function| {
1702 let [count_node] = function.expect_exact_arguments()?;
1703 let count = expect_usize_expression(language, diagnostics, build_ctx, count_node)?;
1704 let out_property = (self_property, count)
1705 .map(|(items, count)| items.into_iter().take(count).collect_vec());
1706 Ok(L::Property::wrap_property(out_property.into_dyn()))
1707 },
1708 );
1709 map
1710}
1711
1712fn build_filter_operation<'a, L, O, P, B>(
1714 language: &L,
1715 diagnostics: &mut TemplateDiagnostics,
1716 build_ctx: &BuildContext<L::Property>,
1717 self_property: P,
1718 function: &FunctionCallNode,
1719) -> TemplateParseResult<BoxedTemplateProperty<'a, B>>
1720where
1721 L: TemplateLanguage<'a> + ?Sized,
1722 L::Property: WrapTemplateProperty<'a, O>,
1723 P: TemplateProperty + 'a,
1724 P::Output: IntoIterator<Item = O>,
1725 O: Clone + 'a,
1726 B: FromIterator<O>,
1727{
1728 let [lambda_node] = function.expect_exact_arguments()?;
1729 let item_placeholder = PropertyPlaceholder::new();
1730 let item_predicate =
1731 template_parser::catch_aliases(diagnostics, lambda_node, |diagnostics, node| {
1732 let lambda = template_parser::expect_lambda(node)?;
1733 build_lambda_expression(
1734 build_ctx,
1735 lambda,
1736 &[&|| item_placeholder.clone().into_dyn_wrapped()],
1737 |build_ctx, body| expect_boolean_expression(language, diagnostics, build_ctx, body),
1738 )
1739 })?;
1740 let out_property = self_property.and_then(move |items| {
1741 items
1742 .into_iter()
1743 .filter_map(|item| {
1744 item_placeholder.set(item);
1746 let result = item_predicate.extract();
1747 let item = item_placeholder.take().unwrap();
1748 result.map(|pred| pred.then_some(item)).transpose()
1749 })
1750 .collect()
1751 });
1752 Ok(out_property.into_dyn())
1753}
1754
1755fn build_map_operation<'a, L, O, P>(
1758 language: &L,
1759 diagnostics: &mut TemplateDiagnostics,
1760 build_ctx: &BuildContext<L::Property>,
1761 self_property: P,
1762 function: &FunctionCallNode,
1763) -> TemplateParseResult<BoxedAnyProperty<'a>>
1764where
1765 L: TemplateLanguage<'a> + ?Sized,
1766 L::Property: WrapTemplateProperty<'a, O>,
1767 P: TemplateProperty + 'a,
1768 P::Output: IntoIterator<Item = O>,
1769 O: Clone + 'a,
1770{
1771 let [lambda_node] = function.expect_exact_arguments()?;
1772 let item_placeholder = PropertyPlaceholder::new();
1773 let mapped_item =
1774 template_parser::catch_aliases(diagnostics, lambda_node, |diagnostics, node| {
1775 let lambda = template_parser::expect_lambda(node)?;
1776 build_lambda_expression(
1777 build_ctx,
1778 lambda,
1779 &[&|| item_placeholder.clone().into_dyn_wrapped()],
1780 |build_ctx, body| expect_any_expression(language, diagnostics, build_ctx, body),
1781 )
1782 })?;
1783 let mapped_list = ListMapProperty::new(self_property, item_placeholder, mapped_item);
1784 Ok(Box::new(mapped_list))
1785}
1786
1787fn build_any_operation<'a, L, O, P>(
1790 language: &L,
1791 diagnostics: &mut TemplateDiagnostics,
1792 build_ctx: &BuildContext<L::Property>,
1793 self_property: P,
1794 function: &FunctionCallNode,
1795) -> TemplateParseResult<BoxedTemplateProperty<'a, bool>>
1796where
1797 L: TemplateLanguage<'a> + ?Sized,
1798 L::Property: WrapTemplateProperty<'a, O>,
1799 P: TemplateProperty + 'a,
1800 P::Output: IntoIterator<Item = O>,
1801 O: Clone + 'a,
1802{
1803 let [lambda_node] = function.expect_exact_arguments()?;
1804 let item_placeholder = PropertyPlaceholder::new();
1805 let item_predicate =
1806 template_parser::catch_aliases(diagnostics, lambda_node, |diagnostics, node| {
1807 let lambda = template_parser::expect_lambda(node)?;
1808 build_lambda_expression(
1809 build_ctx,
1810 lambda,
1811 &[&|| item_placeholder.clone().into_dyn_wrapped()],
1812 |build_ctx, body| expect_boolean_expression(language, diagnostics, build_ctx, body),
1813 )
1814 })?;
1815
1816 let out_property = self_property.and_then(move |items| {
1817 items
1818 .into_iter()
1819 .map(|item| item_placeholder.with_value(item, || item_predicate.extract()))
1820 .process_results(|mut predicates| predicates.any(|p| p))
1821 });
1822 Ok(out_property.into_dyn())
1823}
1824
1825fn build_all_operation<'a, L, O, P>(
1828 language: &L,
1829 diagnostics: &mut TemplateDiagnostics,
1830 build_ctx: &BuildContext<L::Property>,
1831 self_property: P,
1832 function: &FunctionCallNode,
1833) -> TemplateParseResult<BoxedTemplateProperty<'a, bool>>
1834where
1835 L: TemplateLanguage<'a> + ?Sized,
1836 L::Property: WrapTemplateProperty<'a, O>,
1837 P: TemplateProperty + 'a,
1838 P::Output: IntoIterator<Item = O>,
1839 O: Clone + 'a,
1840{
1841 let [lambda_node] = function.expect_exact_arguments()?;
1842 let item_placeholder = PropertyPlaceholder::new();
1843 let item_predicate =
1844 template_parser::catch_aliases(diagnostics, lambda_node, |diagnostics, node| {
1845 let lambda = template_parser::expect_lambda(node)?;
1846 build_lambda_expression(
1847 build_ctx,
1848 lambda,
1849 &[&|| item_placeholder.clone().into_dyn_wrapped()],
1850 |build_ctx, body| expect_boolean_expression(language, diagnostics, build_ctx, body),
1851 )
1852 })?;
1853
1854 let out_property = self_property.and_then(move |items| {
1855 items
1856 .into_iter()
1857 .map(|item| item_placeholder.with_value(item, || item_predicate.extract()))
1858 .process_results(|mut predicates| predicates.all(|p| p))
1859 });
1860 Ok(out_property.into_dyn())
1861}
1862
1863fn build_lambda_expression<'i, P, T>(
1866 build_ctx: &BuildContext<'i, P>,
1867 lambda: &LambdaNode<'i>,
1868 arg_fns: &[&'i dyn Fn() -> P],
1869 build_body: impl FnOnce(&BuildContext<'i, P>, &ExpressionNode<'i>) -> TemplateParseResult<T>,
1870) -> TemplateParseResult<T> {
1871 if lambda.params.len() != arg_fns.len() {
1872 return Err(TemplateParseError::expression(
1873 format!("Expected {} lambda parameters", arg_fns.len()),
1874 lambda.params_span,
1875 ));
1876 }
1877 let mut local_variables = build_ctx.local_variables.clone();
1878 local_variables.extend(iter::zip(&lambda.params, arg_fns));
1879 let inner_build_ctx = BuildContext {
1880 local_variables,
1881 self_variable: build_ctx.self_variable,
1882 };
1883 build_body(&inner_build_ctx, &lambda.body)
1884}
1885
1886fn builtin_functions<'a, L: TemplateLanguage<'a> + ?Sized>() -> TemplateBuildFunctionFnMap<'a, L> {
1887 let mut map = TemplateBuildFunctionFnMap::<L>::new();
1890 map.insert("fill", |language, diagnostics, build_ctx, function| {
1891 let [width_node, content_node] = function.expect_exact_arguments()?;
1892 let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
1893 let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1894 let template =
1895 ReformatTemplate::new(content, move |formatter, recorded| match width.extract() {
1896 Ok(width) => text_util::write_wrapped(formatter.as_mut(), recorded, width),
1897 Err(err) => formatter.handle_error(err),
1898 });
1899 Ok(L::Property::wrap_template(Box::new(template)))
1900 });
1901 map.insert("indent", |language, diagnostics, build_ctx, function| {
1902 let [prefix_node, content_node] = function.expect_exact_arguments()?;
1903 let prefix = expect_template_expression(language, diagnostics, build_ctx, prefix_node)?;
1904 let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1905 let template = ReformatTemplate::new(content, move |formatter, recorded| {
1906 let rewrap = formatter.rewrap_fn();
1907 text_util::write_indented(formatter.as_mut(), recorded, |formatter| {
1908 prefix.format(&mut rewrap(formatter))
1909 })
1910 });
1911 Ok(L::Property::wrap_template(Box::new(template)))
1912 });
1913 map.insert("pad_start", |language, diagnostics, build_ctx, function| {
1914 let ([width_node, content_node], [fill_char_node]) =
1915 function.expect_named_arguments(&["", "", "fill_char"])?;
1916 let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
1917 let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1918 let fill_char = fill_char_node
1919 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1920 .transpose()?;
1921 let template = new_pad_template(content, fill_char, width, text_util::write_padded_start);
1922 Ok(L::Property::wrap_template(template))
1923 });
1924 map.insert("pad_end", |language, diagnostics, build_ctx, function| {
1925 let ([width_node, content_node], [fill_char_node]) =
1926 function.expect_named_arguments(&["", "", "fill_char"])?;
1927 let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
1928 let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1929 let fill_char = fill_char_node
1930 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1931 .transpose()?;
1932 let template = new_pad_template(content, fill_char, width, text_util::write_padded_end);
1933 Ok(L::Property::wrap_template(template))
1934 });
1935 map.insert(
1936 "pad_centered",
1937 |language, diagnostics, build_ctx, function| {
1938 let ([width_node, content_node], [fill_char_node]) =
1939 function.expect_named_arguments(&["", "", "fill_char"])?;
1940 let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
1941 let content =
1942 expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1943 let fill_char = fill_char_node
1944 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1945 .transpose()?;
1946 let template =
1947 new_pad_template(content, fill_char, width, text_util::write_padded_centered);
1948 Ok(L::Property::wrap_template(template))
1949 },
1950 );
1951 map.insert(
1952 "truncate_start",
1953 |language, diagnostics, build_ctx, function| {
1954 let ([width_node, content_node], [ellipsis_node]) =
1955 function.expect_named_arguments(&["", "", "ellipsis"])?;
1956 let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
1957 let content =
1958 expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1959 let ellipsis = ellipsis_node
1960 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1961 .transpose()?;
1962 let template =
1963 new_truncate_template(content, ellipsis, width, text_util::write_truncated_start);
1964 Ok(L::Property::wrap_template(template))
1965 },
1966 );
1967 map.insert(
1968 "truncate_end",
1969 |language, diagnostics, build_ctx, function| {
1970 let ([width_node, content_node], [ellipsis_node]) =
1971 function.expect_named_arguments(&["", "", "ellipsis"])?;
1972 let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
1973 let content =
1974 expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1975 let ellipsis = ellipsis_node
1976 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
1977 .transpose()?;
1978 let template =
1979 new_truncate_template(content, ellipsis, width, text_util::write_truncated_end);
1980 Ok(L::Property::wrap_template(template))
1981 },
1982 );
1983 map.insert("hash", |language, diagnostics, build_ctx, function| {
1984 let [content_node] = function.expect_exact_arguments()?;
1985 let content = expect_stringify_expression(language, diagnostics, build_ctx, content_node)?;
1986 let result = content.map(|c| hex_util::encode_hex(blake2b_hash(&c).as_ref()));
1987 Ok(result.into_dyn_wrapped())
1988 });
1989 map.insert("label", |language, diagnostics, build_ctx, function| {
1990 let [label_node, content_node] = function.expect_exact_arguments()?;
1991 let label_property =
1992 expect_stringify_expression(language, diagnostics, build_ctx, label_node)?;
1993 let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
1994 let labels =
1995 label_property.map(|s| s.split_whitespace().map(ToString::to_string).collect());
1996 Ok(L::Property::wrap_template(Box::new(LabelTemplate::new(
1997 content, labels,
1998 ))))
1999 });
2000 map.insert(
2001 "raw_escape_sequence",
2002 |language, diagnostics, build_ctx, function| {
2003 let [content_node] = function.expect_exact_arguments()?;
2004 let content =
2005 expect_template_expression(language, diagnostics, build_ctx, content_node)?;
2006 Ok(L::Property::wrap_template(Box::new(
2007 RawEscapeSequenceTemplate(content),
2008 )))
2009 },
2010 );
2011 map.insert("hyperlink", |language, diagnostics, build_ctx, function| {
2012 let ([url_node, text_node], [fallback_node]) = function.expect_arguments()?;
2013 let url = expect_stringify_expression(language, diagnostics, build_ctx, url_node)?;
2014 let text = expect_template_expression(language, diagnostics, build_ctx, text_node)?;
2015 let fallback = fallback_node
2016 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
2017 .transpose()?;
2018 Ok(L::Property::wrap_template(Box::new(
2019 HyperlinkTemplate::new(url, text, fallback),
2020 )))
2021 });
2022 map.insert("stringify", |language, diagnostics, build_ctx, function| {
2023 let [content_node] = function.expect_exact_arguments()?;
2024 let content = expect_stringify_expression(language, diagnostics, build_ctx, content_node)?;
2025 Ok(L::Property::wrap_property(content))
2026 });
2027 map.insert("json", |language, diagnostics, build_ctx, function| {
2028 let [value_node] = function.expect_exact_arguments()?;
2032 let value = expect_serialize_expression(language, diagnostics, build_ctx, value_node)?;
2033 let out_property = value.and_then(|v| Ok(serde_json::to_string(&v)?));
2034 Ok(out_property.into_dyn_wrapped())
2035 });
2036 map.insert("if", |language, diagnostics, build_ctx, function| {
2037 let ([condition_node, true_node], [false_node]) = function.expect_arguments()?;
2038 let condition =
2039 expect_boolean_expression(language, diagnostics, build_ctx, condition_node)?;
2040 let true_any = expect_any_expression(language, diagnostics, build_ctx, true_node)?;
2041 let false_any = false_node
2042 .map(|node| expect_any_expression(language, diagnostics, build_ctx, node))
2043 .transpose()?;
2044 let property = ConditionalProperty::new(condition, true_any, false_any);
2045 Ok(L::Property::wrap_any(Box::new(property)))
2046 });
2047 map.insert("coalesce", |language, diagnostics, build_ctx, function| {
2048 let ([], content_nodes) = function.expect_some_arguments()?;
2049 let contents = content_nodes
2050 .iter()
2051 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
2052 .try_collect()?;
2053 Ok(L::Property::wrap_template(Box::new(CoalesceTemplate(
2054 contents,
2055 ))))
2056 });
2057 map.insert("concat", |language, diagnostics, build_ctx, function| {
2058 let ([], content_nodes) = function.expect_some_arguments()?;
2059 let contents = content_nodes
2060 .iter()
2061 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
2062 .try_collect()?;
2063 Ok(L::Property::wrap_template(Box::new(ConcatTemplate(
2064 contents,
2065 ))))
2066 });
2067 map.insert("join", |language, diagnostics, build_ctx, function| {
2068 let ([separator_node], content_nodes) = function.expect_some_arguments()?;
2069 let separator =
2070 expect_template_expression(language, diagnostics, build_ctx, separator_node)?;
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(JoinTemplate::new(
2076 separator, contents,
2077 ))))
2078 });
2079 map.insert("separate", |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(SeparateTemplate::new(
2088 separator, contents,
2089 ))))
2090 });
2091 map.insert("surround", |language, diagnostics, build_ctx, function| {
2092 let [prefix_node, suffix_node, content_node] = function.expect_exact_arguments()?;
2093 let prefix = expect_template_expression(language, diagnostics, build_ctx, prefix_node)?;
2094 let suffix = expect_template_expression(language, diagnostics, build_ctx, suffix_node)?;
2095 let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
2096 let template = ReformatTemplate::new(content, move |formatter, recorded| {
2097 if recorded.data().is_empty() {
2098 return Ok(());
2099 }
2100 prefix.format(formatter)?;
2101 recorded.replay(formatter.as_mut())?;
2102 suffix.format(formatter)?;
2103 Ok(())
2104 });
2105 Ok(L::Property::wrap_template(Box::new(template)))
2106 });
2107 map.insert("config", |language, diagnostics, _build_ctx, function| {
2108 let [name_node] = function.expect_exact_arguments()?;
2111 let name: ConfigNamePathBuf =
2112 template_parser::catch_aliases(diagnostics, name_node, |_diagnostics, node| {
2113 let name = template_parser::expect_string_literal(node)?;
2114 name.parse().map_err(|err| {
2115 TemplateParseError::expression("Failed to parse config name", node.span)
2116 .with_source(err)
2117 })
2118 })?;
2119 let value = language
2120 .settings()
2121 .get_value(&name)
2122 .optional()
2123 .map_err(|err| {
2124 TemplateParseError::expression("Failed to get config value", function.name_span)
2125 .with_source(err)
2126 })?;
2127 Ok(Literal(value.map(|v| v.decorated("", ""))).into_dyn_wrapped())
2129 });
2130 map
2131}
2132
2133fn new_pad_template<'a, W>(
2134 content: Box<dyn Template + 'a>,
2135 fill_char: Option<Box<dyn Template + 'a>>,
2136 width: BoxedTemplateProperty<'a, usize>,
2137 write_padded: W,
2138) -> Box<dyn Template + 'a>
2139where
2140 W: Fn(&mut dyn Formatter, &FormatRecorder, &FormatRecorder, usize) -> io::Result<()> + 'a,
2141{
2142 let default_fill_char = FormatRecorder::with_data(" ");
2143 let template = ReformatTemplate::new(content, move |formatter, recorded| {
2144 let width = match width.extract() {
2145 Ok(width) => width,
2146 Err(err) => return formatter.handle_error(err),
2147 };
2148 let mut fill_char_recorder;
2149 let recorded_fill_char = if let Some(fill_char) = &fill_char {
2150 let rewrap = formatter.rewrap_fn();
2151 fill_char_recorder = FormatRecorder::new(formatter.maybe_color());
2152 fill_char.format(&mut rewrap(&mut fill_char_recorder))?;
2153 &fill_char_recorder
2154 } else {
2155 &default_fill_char
2156 };
2157 write_padded(formatter.as_mut(), recorded, recorded_fill_char, width)
2158 });
2159 Box::new(template)
2160}
2161
2162fn new_truncate_template<'a, W>(
2163 content: Box<dyn Template + 'a>,
2164 ellipsis: Option<Box<dyn Template + 'a>>,
2165 width: BoxedTemplateProperty<'a, usize>,
2166 write_truncated: W,
2167) -> Box<dyn Template + 'a>
2168where
2169 W: Fn(&mut dyn Formatter, &FormatRecorder, &FormatRecorder, usize) -> io::Result<usize> + 'a,
2170{
2171 let default_ellipsis = FormatRecorder::with_data("");
2172 let template = ReformatTemplate::new(content, move |formatter, recorded| {
2173 let width = match width.extract() {
2174 Ok(width) => width,
2175 Err(err) => return formatter.handle_error(err),
2176 };
2177 let mut ellipsis_recorder;
2178 let recorded_ellipsis = if let Some(ellipsis) = &ellipsis {
2179 let rewrap = formatter.rewrap_fn();
2180 ellipsis_recorder = FormatRecorder::new(formatter.maybe_color());
2181 ellipsis.format(&mut rewrap(&mut ellipsis_recorder))?;
2182 &ellipsis_recorder
2183 } else {
2184 &default_ellipsis
2185 };
2186 write_truncated(formatter.as_mut(), recorded, recorded_ellipsis, width)?;
2187 Ok(())
2188 });
2189 Box::new(template)
2190}
2191
2192pub fn build_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2194 language: &L,
2195 diagnostics: &mut TemplateDiagnostics,
2196 build_ctx: &BuildContext<L::Property>,
2197 node: &ExpressionNode,
2198) -> TemplateParseResult<Expression<L::Property>> {
2199 template_parser::catch_aliases(diagnostics, node, |diagnostics, node| match &node.kind {
2200 ExpressionKind::Identifier(name) => {
2201 if let Some(make) = build_ctx.local_variables.get(name) {
2202 Ok(Expression::unlabeled(make()))
2204 } else if *name == "self" {
2205 let make = build_ctx.self_variable;
2207 Ok(Expression::unlabeled(make()))
2208 } else {
2209 let property = build_keyword(language, diagnostics, build_ctx, name, node.span)
2210 .map_err(|err| {
2211 err.extend_keyword_candidates(itertools::chain(
2212 build_ctx.local_variables.keys().copied(),
2213 ["self"],
2214 ))
2215 })?;
2216 Ok(Expression::with_label(property, *name))
2217 }
2218 }
2219 ExpressionKind::Boolean(value) => {
2220 let property = Literal(*value).into_dyn_wrapped();
2221 Ok(Expression::unlabeled(property))
2222 }
2223 ExpressionKind::Integer(value) => {
2224 let property = Literal(*value).into_dyn_wrapped();
2225 Ok(Expression::unlabeled(property))
2226 }
2227 ExpressionKind::String(value) => {
2228 let property = Literal(value.clone()).into_dyn_wrapped();
2229 Ok(Expression::unlabeled(property))
2230 }
2231 ExpressionKind::Pattern(_) => Err(TemplateParseError::expression(
2232 "String patterns may not be used as expression values",
2233 node.span,
2234 )),
2235 ExpressionKind::Unary(op, arg_node) => {
2236 let property = build_unary_operation(language, diagnostics, build_ctx, *op, arg_node)?;
2237 Ok(Expression::unlabeled(property))
2238 }
2239 ExpressionKind::Binary(op, lhs_node, rhs_node) => {
2240 let property = build_binary_operation(
2241 language,
2242 diagnostics,
2243 build_ctx,
2244 *op,
2245 lhs_node,
2246 rhs_node,
2247 node.span,
2248 )?;
2249 Ok(Expression::unlabeled(property))
2250 }
2251 ExpressionKind::Concat(nodes) => {
2252 let templates = nodes
2253 .iter()
2254 .map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
2255 .try_collect()?;
2256 let property = L::Property::wrap_template(Box::new(ConcatTemplate(templates)));
2257 Ok(Expression::unlabeled(property))
2258 }
2259 ExpressionKind::FunctionCall(function) => {
2260 let property = language.build_function(diagnostics, build_ctx, function)?;
2261 Ok(Expression::unlabeled(property))
2262 }
2263 ExpressionKind::MethodCall(method) => {
2264 let mut expression =
2265 build_expression(language, diagnostics, build_ctx, &method.object)?;
2266 expression.property = language.build_method(
2267 diagnostics,
2268 build_ctx,
2269 expression.property,
2270 &method.function,
2271 )?;
2272 expression.labels.push(method.function.name.to_owned());
2273 Ok(expression)
2274 }
2275 ExpressionKind::Lambda(_) => Err(TemplateParseError::expression(
2276 "Lambda cannot be defined here",
2277 node.span,
2278 )),
2279 ExpressionKind::AliasExpanded(..) => unreachable!(),
2280 })
2281}
2282
2283pub fn build<'a, C, L>(
2285 language: &L,
2286 diagnostics: &mut TemplateDiagnostics,
2287 node: &ExpressionNode,
2288) -> TemplateParseResult<TemplateRenderer<'a, C>>
2289where
2290 C: Clone + 'a,
2291 L: TemplateLanguage<'a> + ?Sized,
2292 L::Property: WrapTemplateProperty<'a, C>,
2293{
2294 let self_placeholder = PropertyPlaceholder::new();
2295 let build_ctx = BuildContext {
2296 local_variables: HashMap::new(),
2297 self_variable: &|| self_placeholder.clone().into_dyn_wrapped(),
2298 };
2299 let template = expect_template_expression(language, diagnostics, &build_ctx, node)?;
2300 Ok(TemplateRenderer::new(template, self_placeholder))
2301}
2302
2303pub fn parse<'a, C, L>(
2305 language: &L,
2306 diagnostics: &mut TemplateDiagnostics,
2307 template_text: &str,
2308 aliases_map: &TemplateAliasesMap,
2309) -> TemplateParseResult<TemplateRenderer<'a, C>>
2310where
2311 C: Clone + 'a,
2312 L: TemplateLanguage<'a> + ?Sized,
2313 L::Property: WrapTemplateProperty<'a, C>,
2314{
2315 let node = template_parser::parse(template_text, aliases_map)?;
2316 build(language, diagnostics, &node).map_err(|err| err.extend_alias_candidates(aliases_map))
2317}
2318
2319pub fn expect_boolean_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2320 language: &L,
2321 diagnostics: &mut TemplateDiagnostics,
2322 build_ctx: &BuildContext<L::Property>,
2323 node: &ExpressionNode,
2324) -> TemplateParseResult<BoxedTemplateProperty<'a, bool>> {
2325 expect_expression_of_type(
2326 language,
2327 diagnostics,
2328 build_ctx,
2329 node,
2330 "Boolean",
2331 |expression| expression.try_into_boolean(),
2332 )
2333}
2334
2335pub fn expect_integer_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2336 language: &L,
2337 diagnostics: &mut TemplateDiagnostics,
2338 build_ctx: &BuildContext<L::Property>,
2339 node: &ExpressionNode,
2340) -> TemplateParseResult<BoxedTemplateProperty<'a, i64>> {
2341 expect_expression_of_type(
2342 language,
2343 diagnostics,
2344 build_ctx,
2345 node,
2346 "Integer",
2347 |expression| expression.try_into_integer(),
2348 )
2349}
2350
2351pub fn expect_isize_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2353 language: &L,
2354 diagnostics: &mut TemplateDiagnostics,
2355 build_ctx: &BuildContext<L::Property>,
2356 node: &ExpressionNode,
2357) -> TemplateParseResult<BoxedTemplateProperty<'a, isize>> {
2358 let i64_property = expect_integer_expression(language, diagnostics, build_ctx, node)?;
2359 let isize_property = i64_property.and_then(|v| Ok(isize::try_from(v)?));
2360 Ok(isize_property.into_dyn())
2361}
2362
2363pub fn expect_usize_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2365 language: &L,
2366 diagnostics: &mut TemplateDiagnostics,
2367 build_ctx: &BuildContext<L::Property>,
2368 node: &ExpressionNode,
2369) -> TemplateParseResult<BoxedTemplateProperty<'a, usize>> {
2370 let i64_property = expect_integer_expression(language, diagnostics, build_ctx, node)?;
2371 let usize_property = i64_property.and_then(|v| Ok(usize::try_from(v)?));
2372 Ok(usize_property.into_dyn())
2373}
2374
2375pub fn expect_stringify_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2376 language: &L,
2377 diagnostics: &mut TemplateDiagnostics,
2378 build_ctx: &BuildContext<L::Property>,
2379 node: &ExpressionNode,
2380) -> TemplateParseResult<BoxedTemplateProperty<'a, String>> {
2381 expect_expression_of_type(
2384 language,
2385 diagnostics,
2386 build_ctx,
2387 node,
2388 "Stringify",
2389 |expression| expression.try_into_stringify(),
2390 )
2391}
2392
2393pub fn expect_timestamp_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, Timestamp>> {
2399 expect_expression_of_type(
2400 language,
2401 diagnostics,
2402 build_ctx,
2403 node,
2404 "Timestamp",
2405 |expression| expression.try_into_timestamp(),
2406 )
2407}
2408
2409pub fn expect_serialize_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2410 language: &L,
2411 diagnostics: &mut TemplateDiagnostics,
2412 build_ctx: &BuildContext<L::Property>,
2413 node: &ExpressionNode,
2414) -> TemplateParseResult<BoxedSerializeProperty<'a>> {
2415 expect_expression_of_type(
2416 language,
2417 diagnostics,
2418 build_ctx,
2419 node,
2420 "Serialize",
2421 |expression| expression.try_into_serialize(),
2422 )
2423}
2424
2425pub fn expect_template_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2426 language: &L,
2427 diagnostics: &mut TemplateDiagnostics,
2428 build_ctx: &BuildContext<L::Property>,
2429 node: &ExpressionNode,
2430) -> TemplateParseResult<Box<dyn Template + 'a>> {
2431 expect_expression_of_type(
2432 language,
2433 diagnostics,
2434 build_ctx,
2435 node,
2436 "Template",
2437 |expression| expression.try_into_template(),
2438 )
2439}
2440
2441pub fn expect_any_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
2442 language: &L,
2443 diagnostics: &mut TemplateDiagnostics,
2444 build_ctx: &BuildContext<L::Property>,
2445 node: &ExpressionNode,
2446) -> TemplateParseResult<BoxedAnyProperty<'a>> {
2447 template_parser::catch_aliases(diagnostics, node, |diagnostics, node| {
2448 Ok(
2449 Box::new(build_expression(language, diagnostics, build_ctx, node)?)
2450 as BoxedAnyProperty<'a>,
2451 )
2452 })
2453}
2454
2455fn expect_expression_of_type<'a, L: TemplateLanguage<'a> + ?Sized, T>(
2456 language: &L,
2457 diagnostics: &mut TemplateDiagnostics,
2458 build_ctx: &BuildContext<L::Property>,
2459 node: &ExpressionNode,
2460 expected_type: &str,
2461 f: impl FnOnce(Expression<L::Property>) -> Option<T>,
2462) -> TemplateParseResult<T> {
2463 template_parser::catch_aliases(diagnostics, node, |diagnostics, node| {
2464 let expression = build_expression(language, diagnostics, build_ctx, node)?;
2465 let actual_type = expression.type_name();
2466 f(expression)
2467 .ok_or_else(|| TemplateParseError::expected_type(expected_type, actual_type, node.span))
2468 })
2469}
2470
2471#[cfg(test)]
2472mod tests {
2473 use assert_matches::assert_matches;
2474 use jj_lib::backend::MillisSinceEpoch;
2475 use jj_lib::config::StackedConfig;
2476
2477 use super::*;
2478 use crate::formatter;
2479 use crate::formatter::ColorFormatter;
2480 use crate::generic_templater;
2481 use crate::generic_templater::GenericTemplateLanguage;
2482
2483 #[derive(Clone, Debug, serde::Serialize)]
2484 struct Context;
2485
2486 type TestTemplateLanguage = GenericTemplateLanguage<'static, Context>;
2487 type TestTemplatePropertyKind = <TestTemplateLanguage as TemplateLanguage<'static>>::Property;
2488
2489 generic_templater::impl_self_property_wrapper!(Context);
2490
2491 struct TestTemplateEnv {
2493 language: TestTemplateLanguage,
2494 aliases_map: TemplateAliasesMap,
2495 color_rules: Vec<(Vec<String>, formatter::Style)>,
2496 }
2497
2498 impl TestTemplateEnv {
2499 fn new() -> Self {
2500 Self::with_config(StackedConfig::with_defaults())
2501 }
2502
2503 fn with_config(config: StackedConfig) -> Self {
2504 let settings = UserSettings::from_config(config).unwrap();
2505 Self {
2506 language: TestTemplateLanguage::new(&settings),
2507 aliases_map: TemplateAliasesMap::new(),
2508 color_rules: Vec::new(),
2509 }
2510 }
2511 }
2512
2513 impl TestTemplateEnv {
2514 fn add_keyword<F>(&mut self, name: &'static str, build: F)
2515 where
2516 F: Fn() -> TestTemplatePropertyKind + 'static,
2517 {
2518 self.language.add_keyword(name, move |_| Ok(build()));
2519 }
2520
2521 fn add_alias(&mut self, decl: impl AsRef<str>, defn: impl Into<String>) {
2522 self.aliases_map.insert(decl, defn).unwrap();
2523 }
2524
2525 fn add_color(&mut self, label: &str, fg: crossterm::style::Color) {
2526 let labels = label.split_whitespace().map(|s| s.to_owned()).collect();
2527 let style = formatter::Style {
2528 fg: Some(fg),
2529 ..Default::default()
2530 };
2531 self.color_rules.push((labels, style));
2532 }
2533
2534 fn parse(&self, template: &str) -> TemplateParseResult<TemplateRenderer<'static, Context>> {
2535 parse(
2536 &self.language,
2537 &mut TemplateDiagnostics::new(),
2538 template,
2539 &self.aliases_map,
2540 )
2541 }
2542
2543 fn parse_err(&self, template: &str) -> String {
2544 let err = self
2545 .parse(template)
2546 .err()
2547 .expect("Got unexpected successful template rendering");
2548
2549 iter::successors(Some(&err as &dyn std::error::Error), |e| e.source()).join("\n")
2550 }
2551
2552 fn parse_err_kind(&self, template: &str) -> TemplateParseErrorKind {
2553 self.parse(template)
2554 .err()
2555 .expect("Got unexpected successful template rendering")
2556 .kind()
2557 .clone()
2558 }
2559
2560 fn render_ok(&self, template: &str) -> String {
2561 let template = self.parse(template).unwrap();
2562 let mut output = Vec::new();
2563 let mut formatter =
2564 ColorFormatter::new(&mut output, self.color_rules.clone().into(), false);
2565 template.format(&Context, &mut formatter).unwrap();
2566 drop(formatter);
2567 String::from_utf8(output).unwrap()
2568 }
2569
2570 fn render_plain(&self, template: &str) -> String {
2571 let template = self.parse(template).unwrap();
2572 String::from_utf8(template.format_plain_text(&Context)).unwrap()
2573 }
2574 }
2575
2576 fn literal<'a, O>(value: O) -> TestTemplatePropertyKind
2577 where
2578 O: Clone + 'a,
2579 TestTemplatePropertyKind: WrapTemplateProperty<'a, O>,
2580 {
2581 Literal(value).into_dyn_wrapped()
2582 }
2583
2584 fn new_error_property<'a, O>(message: &'a str) -> TestTemplatePropertyKind
2585 where
2586 TestTemplatePropertyKind: WrapTemplateProperty<'a, O>,
2587 {
2588 Literal(())
2589 .and_then(|()| Err(TemplatePropertyError(message.into())))
2590 .into_dyn_wrapped()
2591 }
2592
2593 fn new_signature(name: &str, email: &str) -> Signature {
2594 Signature {
2595 name: name.to_owned(),
2596 email: email.to_owned(),
2597 timestamp: new_timestamp(0, 0),
2598 }
2599 }
2600
2601 fn new_timestamp(msec: i64, tz_offset: i32) -> Timestamp {
2602 Timestamp {
2603 timestamp: MillisSinceEpoch(msec),
2604 tz_offset,
2605 }
2606 }
2607
2608 #[test]
2609 fn test_parsed_tree() {
2610 let mut env = TestTemplateEnv::new();
2611 env.add_keyword("divergent", || literal(false));
2612 env.add_keyword("empty", || literal(true));
2613 env.add_keyword("hello", || literal("Hello".to_owned()));
2614
2615 insta::assert_snapshot!(env.render_ok(r#" "#), @"");
2617
2618 insta::assert_snapshot!(env.render_ok(r#" hello.upper() "#), @"HELLO");
2620
2621 insta::assert_snapshot!(env.render_ok(r#" hello.upper() ++ true "#), @"HELLOtrue");
2623
2624 insta::assert_snapshot!(env.render_ok(r#"(hello.upper())"#), @"HELLO");
2626
2627 insta::assert_snapshot!(env.render_ok(r#"(hello.upper() ++ " ") ++ empty"#), @"HELLO true");
2629
2630 insta::assert_snapshot!(env.render_ok(r#"if((divergent), "t", "f")"#), @"f");
2632
2633 insta::assert_snapshot!(env.render_ok(r#"(hello).upper()"#), @"HELLO");
2635
2636 insta::assert_snapshot!(env.render_ok("hello\n .upper()"), @"HELLO");
2638 }
2639
2640 #[test]
2641 fn test_parse_error() {
2642 let mut env = TestTemplateEnv::new();
2643 env.add_keyword("description", || literal("".to_owned()));
2644 env.add_keyword("empty", || literal(true));
2645
2646 insta::assert_snapshot!(env.parse_err(r#"foo bar"#), @"
2647 --> 1:5
2648 |
2649 1 | foo bar
2650 | ^---
2651 |
2652 = expected <EOI>, `++`, `||`, `&&`, `==`, `!=`, `>=`, `>`, `<=`, `<`, `+`, `-`, `*`, `/`, or `%`
2653 ");
2654 insta::assert_snapshot!(env.parse_err("1 +"), @"
2655 --> 1:4
2656 |
2657 1 | 1 +
2658 | ^---
2659 |
2660 = expected `!`, `-`, or <primary>
2661 ");
2662 insta::assert_snapshot!(env.parse_err("self.timestamp"), @"
2663 --> 1:6
2664 |
2665 1 | self.timestamp
2666 | ^---
2667 |
2668 = expected <function>
2669 ");
2670
2671 insta::assert_snapshot!(env.parse_err(r#"foo"#), @"
2672 --> 1:1
2673 |
2674 1 | foo
2675 | ^-^
2676 |
2677 = Keyword `foo` doesn't exist
2678 ");
2679
2680 insta::assert_snapshot!(env.parse_err(r#"foo()"#), @"
2681 --> 1:1
2682 |
2683 1 | foo()
2684 | ^-^
2685 |
2686 = Function `foo` doesn't exist
2687 ");
2688 insta::assert_snapshot!(env.parse_err(r#"false()"#), @"
2689 --> 1:1
2690 |
2691 1 | false()
2692 | ^---^
2693 |
2694 = Expected identifier
2695 ");
2696
2697 insta::assert_snapshot!(env.parse_err(r#"!foo"#), @"
2698 --> 1:2
2699 |
2700 1 | !foo
2701 | ^-^
2702 |
2703 = Keyword `foo` doesn't exist
2704 ");
2705 insta::assert_snapshot!(env.parse_err(r#"true && 123"#), @"
2706 --> 1:9
2707 |
2708 1 | true && 123
2709 | ^-^
2710 |
2711 = Expected expression of type `Boolean`, but actual type is `Integer`
2712 ");
2713 insta::assert_snapshot!(env.parse_err(r#"true == 1"#), @"
2714 --> 1:1
2715 |
2716 1 | true == 1
2717 | ^-------^
2718 |
2719 = Cannot compare expressions of type `Boolean` and `Integer`
2720 ");
2721 insta::assert_snapshot!(env.parse_err(r#"true != 'a'"#), @"
2722 --> 1:1
2723 |
2724 1 | true != 'a'
2725 | ^---------^
2726 |
2727 = Cannot compare expressions of type `Boolean` and `String`
2728 ");
2729 insta::assert_snapshot!(env.parse_err(r#"1 == true"#), @"
2730 --> 1:1
2731 |
2732 1 | 1 == true
2733 | ^-------^
2734 |
2735 = Cannot compare expressions of type `Integer` and `Boolean`
2736 ");
2737 insta::assert_snapshot!(env.parse_err(r#"1 != 'a'"#), @"
2738 --> 1:1
2739 |
2740 1 | 1 != 'a'
2741 | ^------^
2742 |
2743 = Cannot compare expressions of type `Integer` and `String`
2744 ");
2745 insta::assert_snapshot!(env.parse_err(r#"'a' == true"#), @"
2746 --> 1:1
2747 |
2748 1 | 'a' == true
2749 | ^---------^
2750 |
2751 = Cannot compare expressions of type `String` and `Boolean`
2752 ");
2753 insta::assert_snapshot!(env.parse_err(r#"'a' != 1"#), @"
2754 --> 1:1
2755 |
2756 1 | 'a' != 1
2757 | ^------^
2758 |
2759 = Cannot compare expressions of type `String` and `Integer`
2760 ");
2761 insta::assert_snapshot!(env.parse_err(r#"'a' == label("", "")"#), @r#"
2762 --> 1:1
2763 |
2764 1 | 'a' == label("", "")
2765 | ^------------------^
2766 |
2767 = Cannot compare expressions of type `String` and `Template`
2768 "#);
2769 insta::assert_snapshot!(env.parse_err(r#"'a' > 1"#), @"
2770 --> 1:1
2771 |
2772 1 | 'a' > 1
2773 | ^-----^
2774 |
2775 = Cannot compare expressions of type `String` and `Integer`
2776 ");
2777
2778 insta::assert_snapshot!(env.parse_err(r#"description.first_line().foo()"#), @"
2779 --> 1:26
2780 |
2781 1 | description.first_line().foo()
2782 | ^-^
2783 |
2784 = Method `foo` doesn't exist for type `String`
2785 ");
2786
2787 insta::assert_snapshot!(env.parse_err(r#"10000000000000000000"#), @"
2788 --> 1:1
2789 |
2790 1 | 10000000000000000000
2791 | ^------------------^
2792 |
2793 = Invalid integer literal
2794 number too large to fit in target type
2795 ");
2796 insta::assert_snapshot!(env.parse_err(r#"42.foo()"#), @"
2797 --> 1:4
2798 |
2799 1 | 42.foo()
2800 | ^-^
2801 |
2802 = Method `foo` doesn't exist for type `Integer`
2803 ");
2804 insta::assert_snapshot!(env.parse_err(r#"(-empty)"#), @"
2805 --> 1:3
2806 |
2807 1 | (-empty)
2808 | ^---^
2809 |
2810 = Expected expression of type `Integer`, but actual type is `Boolean`
2811 ");
2812
2813 insta::assert_snapshot!(env.parse_err(r#"("foo" ++ "bar").baz()"#), @r#"
2814 --> 1:18
2815 |
2816 1 | ("foo" ++ "bar").baz()
2817 | ^-^
2818 |
2819 = Method `baz` doesn't exist for type `Template`
2820 "#);
2821
2822 insta::assert_snapshot!(env.parse_err(r#"description.contains()"#), @"
2823 --> 1:22
2824 |
2825 1 | description.contains()
2826 | ^
2827 |
2828 = Function `contains`: Expected 1 arguments
2829 ");
2830
2831 insta::assert_snapshot!(env.parse_err(r#"description.first_line("foo")"#), @r#"
2832 --> 1:24
2833 |
2834 1 | description.first_line("foo")
2835 | ^---^
2836 |
2837 = Function `first_line`: Expected 0 arguments
2838 "#);
2839
2840 insta::assert_snapshot!(env.parse_err(r#"label()"#), @"
2841 --> 1:7
2842 |
2843 1 | label()
2844 | ^
2845 |
2846 = Function `label`: Expected 2 arguments
2847 ");
2848 insta::assert_snapshot!(env.parse_err(r#"label("foo", "bar", "baz")"#), @r#"
2849 --> 1:7
2850 |
2851 1 | label("foo", "bar", "baz")
2852 | ^-----------------^
2853 |
2854 = Function `label`: Expected 2 arguments
2855 "#);
2856
2857 insta::assert_snapshot!(env.parse_err(r#"if()"#), @"
2858 --> 1:4
2859 |
2860 1 | if()
2861 | ^
2862 |
2863 = Function `if`: Expected 2 to 3 arguments
2864 ");
2865 insta::assert_snapshot!(env.parse_err(r#"if("foo", "bar", "baz", "quux")"#), @r#"
2866 --> 1:4
2867 |
2868 1 | if("foo", "bar", "baz", "quux")
2869 | ^-------------------------^
2870 |
2871 = Function `if`: Expected 2 to 3 arguments
2872 "#);
2873
2874 insta::assert_snapshot!(env.parse_err(r#"pad_start("foo", fill_char = "bar", "baz")"#), @r#"
2875 --> 1:37
2876 |
2877 1 | pad_start("foo", fill_char = "bar", "baz")
2878 | ^---^
2879 |
2880 = Function `pad_start`: Positional argument follows keyword argument
2881 "#);
2882
2883 insta::assert_snapshot!(env.parse_err(r#"if(label("foo", "bar"), "baz")"#), @r#"
2884 --> 1:4
2885 |
2886 1 | if(label("foo", "bar"), "baz")
2887 | ^-----------------^
2888 |
2889 = Expected expression of type `Boolean`, but actual type is `Template`
2890 "#);
2891
2892 insta::assert_snapshot!(env.parse_err(r#"|x| description"#), @"
2893 --> 1:1
2894 |
2895 1 | |x| description
2896 | ^-------------^
2897 |
2898 = Lambda cannot be defined here
2899 ");
2900 }
2901
2902 #[test]
2903 fn test_self_keyword() {
2904 let mut env = TestTemplateEnv::new();
2905 env.add_keyword("say_hello", || literal("Hello".to_owned()));
2906
2907 insta::assert_snapshot!(env.render_ok(r#"self.say_hello()"#), @"Hello");
2908 insta::assert_snapshot!(env.parse_err(r#"self"#), @"
2909 --> 1:1
2910 |
2911 1 | self
2912 | ^--^
2913 |
2914 = Expected expression of type `Template`, but actual type is `Self`
2915 ");
2916 }
2917
2918 #[test]
2919 fn test_boolean_cast() {
2920 let mut env = TestTemplateEnv::new();
2921
2922 insta::assert_snapshot!(env.render_ok(r#"if("", true, false)"#), @"false");
2923 insta::assert_snapshot!(env.render_ok(r#"if("a", true, false)"#), @"true");
2924
2925 env.add_keyword("sl0", || literal::<Vec<String>>(vec![]));
2926 env.add_keyword("sl1", || literal(vec!["".to_owned()]));
2927 insta::assert_snapshot!(env.render_ok(r#"if(sl0, true, false)"#), @"false");
2928 insta::assert_snapshot!(env.render_ok(r#"if(sl1, true, false)"#), @"true");
2929
2930 insta::assert_snapshot!(env.parse_err(r#"if(0, true, false)"#), @"
2932 --> 1:4
2933 |
2934 1 | if(0, true, false)
2935 | ^
2936 |
2937 = Expected expression of type `Boolean`, but actual type is `Integer`
2938 ");
2939
2940 env.add_keyword("none_i64", || literal(None::<i64>));
2942 env.add_keyword("some_i64", || literal(Some(0)));
2943 insta::assert_snapshot!(env.render_ok(r#"if(none_i64, true, false)"#), @"false");
2944 insta::assert_snapshot!(env.render_ok(r#"if(some_i64, true, false)"#), @"true");
2945
2946 insta::assert_snapshot!(
2948 env.render_ok("if(-none_i64 == 1, true, false)"),
2949 @"<Error: No Integer available>"
2950 );
2951
2952 insta::assert_snapshot!(env.parse_err(r#"if(label("", ""), true, false)"#), @r#"
2953 --> 1:4
2954 |
2955 1 | if(label("", ""), true, false)
2956 | ^-----------^
2957 |
2958 = Expected expression of type `Boolean`, but actual type is `Template`
2959 "#);
2960 insta::assert_snapshot!(env.parse_err(r#"if(sl0.map(|x| x), true, false)"#), @"
2961 --> 1:4
2962 |
2963 1 | if(sl0.map(|x| x), true, false)
2964 | ^------------^
2965 |
2966 = Expected expression of type `Boolean`, but actual type is `AnyList`
2967 ");
2968
2969 env.add_keyword("empty_email", || literal(Email("".to_owned())));
2970 env.add_keyword("nonempty_email", || {
2971 literal(Email("local@domain".to_owned()))
2972 });
2973 insta::assert_snapshot!(env.render_ok(r#"if(empty_email, true, false)"#), @"false");
2974 insta::assert_snapshot!(env.render_ok(r#"if(nonempty_email, true, false)"#), @"true");
2975
2976 env.add_keyword("config_bool", || literal(ConfigValue::from(true)));
2978 insta::assert_snapshot!(env.parse_err("if(config_bool, true, false)"), @"
2979 --> 1:4
2980 |
2981 1 | if(config_bool, true, false)
2982 | ^---------^
2983 |
2984 = Expected expression of type `Boolean`, but actual type is `ConfigValue`
2985 ");
2986
2987 env.add_keyword("signature", || {
2989 literal(new_signature("Test User", "test.user@example.com"))
2990 });
2991 env.add_keyword("size_hint", || literal((5, None)));
2992 env.add_keyword("timestamp", || literal(new_timestamp(0, 0)));
2993 env.add_keyword("timestamp_range", || {
2994 literal(TimestampRange {
2995 start: new_timestamp(0, 0),
2996 end: new_timestamp(0, 0),
2997 })
2998 });
2999 assert_matches!(
3000 env.parse_err_kind("if(signature, true, false)"),
3001 TemplateParseErrorKind::Expression(_)
3002 );
3003 assert_matches!(
3004 env.parse_err_kind("if(size_hint, true, false)"),
3005 TemplateParseErrorKind::Expression(_)
3006 );
3007 assert_matches!(
3008 env.parse_err_kind("if(timestamp, true, false)"),
3009 TemplateParseErrorKind::Expression(_)
3010 );
3011 assert_matches!(
3012 env.parse_err_kind("if(timestamp_range, true, false)"),
3013 TemplateParseErrorKind::Expression(_)
3014 );
3015 }
3016
3017 #[test]
3018 fn test_arithmetic_operation() {
3019 let mut env = TestTemplateEnv::new();
3020 env.add_keyword("none_i64", || literal(None::<i64>));
3021 env.add_keyword("some_i64", || literal(Some(1)));
3022 env.add_keyword("i64_min", || literal(i64::MIN));
3023 env.add_keyword("i64_max", || literal(i64::MAX));
3024
3025 insta::assert_snapshot!(env.render_ok(r#"-1"#), @"-1");
3026 insta::assert_snapshot!(env.render_ok(r#"--2"#), @"2");
3027 insta::assert_snapshot!(env.render_ok(r#"-(3)"#), @"-3");
3028 insta::assert_snapshot!(env.render_ok(r#"1 + 2"#), @"3");
3029 insta::assert_snapshot!(env.render_ok(r#"2 * 3"#), @"6");
3030 insta::assert_snapshot!(env.render_ok(r#"1 + 2 * 3"#), @"7");
3031 insta::assert_snapshot!(env.render_ok(r#"4 / 2"#), @"2");
3032 insta::assert_snapshot!(env.render_ok(r#"5 / 2"#), @"2");
3033 insta::assert_snapshot!(env.render_ok(r#"5 % 2"#), @"1");
3034
3035 insta::assert_snapshot!(env.render_ok(r#"-none_i64"#), @"<Error: No Integer available>");
3038 insta::assert_snapshot!(env.render_ok(r#"-some_i64"#), @"-1");
3039 insta::assert_snapshot!(env.render_ok(r#"some_i64 + some_i64"#), @"2");
3040 insta::assert_snapshot!(env.render_ok(r#"some_i64 + none_i64"#), @"<Error: No Integer available>");
3041 insta::assert_snapshot!(env.render_ok(r#"none_i64 + some_i64"#), @"<Error: No Integer available>");
3042 insta::assert_snapshot!(env.render_ok(r#"none_i64 + none_i64"#), @"<Error: No Integer available>");
3043
3044 insta::assert_snapshot!(
3046 env.render_ok(r#"-i64_min"#),
3047 @"<Error: Attempt to negate with overflow>");
3048 insta::assert_snapshot!(
3049 env.render_ok(r#"i64_max + 1"#),
3050 @"<Error: Attempt to add with overflow>");
3051 insta::assert_snapshot!(
3052 env.render_ok(r#"i64_min - 1"#),
3053 @"<Error: Attempt to subtract with overflow>");
3054 insta::assert_snapshot!(
3055 env.render_ok(r#"i64_max * 2"#),
3056 @"<Error: Attempt to multiply with overflow>");
3057 insta::assert_snapshot!(
3058 env.render_ok(r#"i64_min / -1"#),
3059 @"<Error: Attempt to divide with overflow>");
3060 insta::assert_snapshot!(
3061 env.render_ok(r#"1 / 0"#),
3062 @"<Error: Attempt to divide by zero>");
3063 insta::assert_snapshot!(
3064 env.render_ok("i64_min % -1"),
3065 @"<Error: Attempt to divide with overflow>");
3066 insta::assert_snapshot!(
3067 env.render_ok(r#"1 % 0"#),
3068 @"<Error: Attempt to divide by zero>");
3069 }
3070
3071 #[test]
3072 fn test_relational_operation() {
3073 let mut env = TestTemplateEnv::new();
3074 env.add_keyword("none_i64", || literal(None::<i64>));
3075 env.add_keyword("some_i64_0", || literal(Some(0_i64)));
3076 env.add_keyword("some_i64_1", || literal(Some(1_i64)));
3077
3078 insta::assert_snapshot!(env.render_ok(r#"1 >= 1"#), @"true");
3079 insta::assert_snapshot!(env.render_ok(r#"0 >= 1"#), @"false");
3080 insta::assert_snapshot!(env.render_ok(r#"2 > 1"#), @"true");
3081 insta::assert_snapshot!(env.render_ok(r#"1 > 1"#), @"false");
3082 insta::assert_snapshot!(env.render_ok(r#"1 <= 1"#), @"true");
3083 insta::assert_snapshot!(env.render_ok(r#"2 <= 1"#), @"false");
3084 insta::assert_snapshot!(env.render_ok(r#"0 < 1"#), @"true");
3085 insta::assert_snapshot!(env.render_ok(r#"1 < 1"#), @"false");
3086
3087 insta::assert_snapshot!(env.render_ok(r#"none_i64 < some_i64_0"#), @"true");
3089 insta::assert_snapshot!(env.render_ok(r#"some_i64_0 > some_i64_1"#), @"false");
3090 insta::assert_snapshot!(env.render_ok(r#"none_i64 < 0"#), @"true");
3091 insta::assert_snapshot!(env.render_ok(r#"1 > some_i64_0"#), @"true");
3092
3093 assert_matches!(
3095 env.parse_err_kind("42 >= true"),
3096 TemplateParseErrorKind::Expression(_)
3097 );
3098 assert_matches!(
3099 env.parse_err_kind("none_i64 >= true"),
3100 TemplateParseErrorKind::Expression(_)
3101 );
3102
3103 env.add_keyword("str_list", || {
3105 literal(vec!["foo".to_owned(), "bar".to_owned()])
3106 });
3107 env.add_keyword("cfg_val", || {
3108 literal(ConfigValue::from_iter([("foo", "bar")]))
3109 });
3110 env.add_keyword("some_cfg", || literal(Some(ConfigValue::from(1))));
3111 env.add_keyword("signature", || {
3112 literal(new_signature("User", "user@example.com"))
3113 });
3114 env.add_keyword("email", || literal(Email("me@example.com".to_owned())));
3115 env.add_keyword("size_hint", || literal((10, None)));
3116 env.add_keyword("timestamp", || literal(new_timestamp(0, 0)));
3117 env.add_keyword("timestamp_range", || {
3118 literal(TimestampRange {
3119 start: new_timestamp(0, 0),
3120 end: new_timestamp(0, 0),
3121 })
3122 });
3123 assert_matches!(
3124 env.parse_err_kind("'a' >= 'a'"),
3125 TemplateParseErrorKind::Expression(_)
3126 );
3127 assert_matches!(
3128 env.parse_err_kind("str_list >= str_list"),
3129 TemplateParseErrorKind::Expression(_)
3130 );
3131 assert_matches!(
3132 env.parse_err_kind("true >= true"),
3133 TemplateParseErrorKind::Expression(_)
3134 );
3135 assert_matches!(
3136 env.parse_err_kind("cfg_val >= cfg_val"),
3137 TemplateParseErrorKind::Expression(_)
3138 );
3139 assert_matches!(
3140 env.parse_err_kind("some_cfg >= some_cfg"),
3141 TemplateParseErrorKind::Expression(_)
3142 );
3143 assert_matches!(
3144 env.parse_err_kind("signature >= signature"),
3145 TemplateParseErrorKind::Expression(_)
3146 );
3147 assert_matches!(
3148 env.parse_err_kind("email >= email"),
3149 TemplateParseErrorKind::Expression(_)
3150 );
3151 assert_matches!(
3152 env.parse_err_kind("size_hint >= size_hint"),
3153 TemplateParseErrorKind::Expression(_)
3154 );
3155 assert_matches!(
3156 env.parse_err_kind("timestamp >= timestamp"),
3157 TemplateParseErrorKind::Expression(_)
3158 );
3159 assert_matches!(
3160 env.parse_err_kind("timestamp_range >= timestamp_range"),
3161 TemplateParseErrorKind::Expression(_)
3162 );
3163 assert_matches!(
3164 env.parse_err_kind("label('', '') >= label('', '')"),
3165 TemplateParseErrorKind::Expression(_)
3166 );
3167 assert_matches!(
3168 env.parse_err_kind("str_list.map(|s| s) >= str_list.map(|s| s)"),
3169 TemplateParseErrorKind::Expression(_)
3170 );
3171 }
3172
3173 #[test]
3174 fn test_logical_operation() {
3175 let mut env = TestTemplateEnv::new();
3176 env.add_keyword("none_i64", || literal::<Option<i64>>(None));
3177 env.add_keyword("some_i64_0", || literal(Some(0_i64)));
3178 env.add_keyword("some_i64_1", || literal(Some(1_i64)));
3179 env.add_keyword("email1", || literal(Email("local-1@domain".to_owned())));
3180 env.add_keyword("email2", || literal(Email("local-2@domain".to_owned())));
3181
3182 insta::assert_snapshot!(env.render_ok(r#"!false"#), @"true");
3183 insta::assert_snapshot!(env.render_ok(r#"false || !false"#), @"true");
3184 insta::assert_snapshot!(env.render_ok(r#"false && true"#), @"false");
3185 insta::assert_snapshot!(env.render_ok(r#"true == true"#), @"true");
3186 insta::assert_snapshot!(env.render_ok(r#"true == false"#), @"false");
3187 insta::assert_snapshot!(env.render_ok(r#"true != true"#), @"false");
3188 insta::assert_snapshot!(env.render_ok(r#"true != false"#), @"true");
3189
3190 insta::assert_snapshot!(env.render_ok(r#"1 == 1"#), @"true");
3191 insta::assert_snapshot!(env.render_ok(r#"1 == 2"#), @"false");
3192 insta::assert_snapshot!(env.render_ok(r#"1 != 1"#), @"false");
3193 insta::assert_snapshot!(env.render_ok(r#"1 != 2"#), @"true");
3194 insta::assert_snapshot!(env.render_ok(r#"none_i64 == none_i64"#), @"true");
3195 insta::assert_snapshot!(env.render_ok(r#"some_i64_0 != some_i64_0"#), @"false");
3196 insta::assert_snapshot!(env.render_ok(r#"none_i64 == 0"#), @"false");
3197 insta::assert_snapshot!(env.render_ok(r#"some_i64_0 != 0"#), @"false");
3198 insta::assert_snapshot!(env.render_ok(r#"1 == some_i64_1"#), @"true");
3199
3200 insta::assert_snapshot!(env.render_ok(r#"'a' == 'a'"#), @"true");
3201 insta::assert_snapshot!(env.render_ok(r#"'a' == 'b'"#), @"false");
3202 insta::assert_snapshot!(env.render_ok(r#"'a' != 'a'"#), @"false");
3203 insta::assert_snapshot!(env.render_ok(r#"'a' != 'b'"#), @"true");
3204 insta::assert_snapshot!(env.render_ok(r#"email1 == email1"#), @"true");
3205 insta::assert_snapshot!(env.render_ok(r#"email1 == email2"#), @"false");
3206 insta::assert_snapshot!(env.render_ok(r#"email1 == 'local-1@domain'"#), @"true");
3207 insta::assert_snapshot!(env.render_ok(r#"email1 != 'local-2@domain'"#), @"true");
3208 insta::assert_snapshot!(env.render_ok(r#"'local-1@domain' == email1"#), @"true");
3209 insta::assert_snapshot!(env.render_ok(r#"'local-2@domain' != email1"#), @"true");
3210
3211 insta::assert_snapshot!(env.render_ok(r#" !"" "#), @"true");
3212 insta::assert_snapshot!(env.render_ok(r#" "" || "a".lines() "#), @"true");
3213
3214 env.add_keyword("bad_bool", || new_error_property::<bool>("Bad"));
3216 insta::assert_snapshot!(env.render_ok(r#"false && bad_bool"#), @"false");
3217 insta::assert_snapshot!(env.render_ok(r#"true && bad_bool"#), @"<Error: Bad>");
3218 insta::assert_snapshot!(env.render_ok(r#"false || bad_bool"#), @"<Error: Bad>");
3219 insta::assert_snapshot!(env.render_ok(r#"true || bad_bool"#), @"true");
3220
3221 assert_matches!(
3223 env.parse_err_kind("some_i64_0 == '0'"),
3224 TemplateParseErrorKind::Expression(_)
3225 );
3226 assert_matches!(
3227 env.parse_err_kind("email1 == 42"),
3228 TemplateParseErrorKind::Expression(_)
3229 );
3230
3231 env.add_keyword("str_list", || {
3233 literal(vec!["foo".to_owned(), "bar".to_owned()])
3234 });
3235 env.add_keyword("cfg_val", || {
3236 literal(ConfigValue::from_iter([("foo", "bar")]))
3237 });
3238 env.add_keyword("some_cfg", || literal(Some(ConfigValue::from(true))));
3239 env.add_keyword("signature", || {
3240 literal(new_signature("User", "user@example.com"))
3241 });
3242 env.add_keyword("size_hint", || literal((10, None)));
3243 env.add_keyword("timestamp", || literal(new_timestamp(0, 0)));
3244 env.add_keyword("timestamp_range", || {
3245 literal(TimestampRange {
3246 start: new_timestamp(0, 0),
3247 end: new_timestamp(0, 0),
3248 })
3249 });
3250 assert_matches!(
3251 env.parse_err_kind("str_list == str_list"),
3252 TemplateParseErrorKind::Expression(_)
3253 );
3254 assert_matches!(
3255 env.parse_err_kind("cfg_val == cfg_val"),
3256 TemplateParseErrorKind::Expression(_)
3257 );
3258 assert_matches!(
3259 env.parse_err_kind("some_cfg == some_cfg"),
3260 TemplateParseErrorKind::Expression(_)
3261 );
3262 assert_matches!(
3263 env.parse_err_kind("signature == signature"),
3264 TemplateParseErrorKind::Expression(_)
3265 );
3266 assert_matches!(
3267 env.parse_err_kind("size_hint == size_hint"),
3268 TemplateParseErrorKind::Expression(_)
3269 );
3270 assert_matches!(
3271 env.parse_err_kind("timestamp == timestamp"),
3272 TemplateParseErrorKind::Expression(_)
3273 );
3274 assert_matches!(
3275 env.parse_err_kind("timestamp_range == timestamp_range"),
3276 TemplateParseErrorKind::Expression(_)
3277 );
3278 assert_matches!(
3279 env.parse_err_kind("label('', '') == label('', '')"),
3280 TemplateParseErrorKind::Expression(_)
3281 );
3282 assert_matches!(
3283 env.parse_err_kind("str_list.map(|s| s) == str_list.map(|s| s)"),
3284 TemplateParseErrorKind::Expression(_)
3285 );
3286 }
3287
3288 #[test]
3289 fn test_list_method() {
3290 let mut env = TestTemplateEnv::new();
3291 env.add_keyword("empty", || literal(true));
3292 env.add_keyword("sep", || literal("sep".to_owned()));
3293
3294 insta::assert_snapshot!(env.render_ok(r#""".lines().len()"#), @"0");
3295 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().len()"#), @"3");
3296
3297 insta::assert_snapshot!(env.render_ok(r#""".lines().join("|")"#), @"");
3298 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().join("|")"#), @"a|b|c");
3299 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().join("\0")"#), @"a\0b\0c");
3301 insta::assert_snapshot!(
3303 env.render_ok(r#""a\nb\nc".lines().join(sep.upper())"#),
3304 @"aSEPbSEPc");
3305
3306 insta::assert_snapshot!(
3307 env.render_ok(r#""a\nbb\nc".lines().filter(|s| s.len() == 1)"#),
3308 @"a c");
3309
3310 insta::assert_snapshot!(
3311 env.render_ok(r#""a\nb\nc".lines().map(|s| s ++ s)"#),
3312 @"aa bb cc");
3313
3314 insta::assert_snapshot!(
3316 env.render_ok(r#""a\nb\nc".lines().any(|s| s == "b")"#),
3317 @"true");
3318 insta::assert_snapshot!(
3319 env.render_ok(r#""a\nb\nc".lines().any(|s| s == "d")"#),
3320 @"false");
3321 insta::assert_snapshot!(
3322 env.render_ok(r#""".lines().any(|s| s == "a")"#),
3323 @"false");
3324 insta::assert_snapshot!(
3326 env.render_ok(r#""ax\nbb\nc".lines().any(|s| s.contains("x"))"#),
3327 @"true");
3328 insta::assert_snapshot!(
3329 env.render_ok(r#""a\nbb\nc".lines().any(|s| s.len() > 1)"#),
3330 @"true");
3331
3332 insta::assert_snapshot!(
3334 env.render_ok(r#""a\nb\nc".lines().all(|s| s.len() == 1)"#),
3335 @"true");
3336 insta::assert_snapshot!(
3337 env.render_ok(r#""a\nbb\nc".lines().all(|s| s.len() == 1)"#),
3338 @"false");
3339 insta::assert_snapshot!(
3341 env.render_ok(r#""".lines().all(|s| s == "a")"#),
3342 @"true");
3343 insta::assert_snapshot!(
3345 env.render_ok(r#""ax\nbx\ncx".lines().all(|s| s.ends_with("x"))"#),
3346 @"true");
3347 insta::assert_snapshot!(
3348 env.render_ok(r#""a\nbb\nc".lines().all(|s| s.len() < 3)"#),
3349 @"true");
3350
3351 insta::assert_snapshot!(
3353 env.render_ok(r#""a\nbb\nccc".lines().filter(|s| s.len() > 1).any(|s| s == "bb")"#),
3354 @"true");
3355 insta::assert_snapshot!(
3356 env.render_ok(r#""a\nbb\nccc".lines().filter(|s| s.len() > 1).all(|s| s.len() >= 2)"#),
3357 @"true");
3358
3359 insta::assert_snapshot!(
3361 env.render_ok(r#"if("a\nb".lines().any(|s| s == "a"), "found", "not found")"#),
3362 @"found");
3363 insta::assert_snapshot!(
3364 env.render_ok(r#"if("a\nb".lines().all(|s| s.len() == 1), "all single", "not all")"#),
3365 @"all single");
3366
3367 insta::assert_snapshot!(
3369 env.render_ok(r#""a\nb\nc".lines().map(|s| s ++ empty)"#),
3370 @"atrue btrue ctrue");
3371 insta::assert_snapshot!(
3373 env.render_ok(r#""a\nb\nc".lines().map(|self| self ++ empty)"#),
3374 @"atrue btrue ctrue");
3375 insta::assert_snapshot!(
3377 env.render_ok(r#""a\nb\nc".lines().map(|empty| empty)"#),
3378 @"a b c");
3379 insta::assert_snapshot!(
3381 env.render_ok(r#""a\nb\nc".lines().map(|s| "x\ny".lines().map(|t| s ++ t))"#),
3382 @"ax ay bx by cx cy");
3383 insta::assert_snapshot!(
3385 env.render_ok(r#""a\nb\nc".lines().map(|s| "x\ny".lines().map(|t| s ++ t).join(",")).join(";")"#),
3386 @"ax,ay;bx,by;cx,cy");
3387 insta::assert_snapshot!(
3389 env.render_ok(r#""! a\n!b\nc\n end".remove_suffix("end").trim_end().lines().map(|s| s.remove_prefix("!").trim_start())"#),
3390 @"a b c");
3391
3392 env.add_alias("identity", "|x| x");
3394 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().map(identity)"#), @"a b c");
3395
3396 insta::assert_snapshot!(env.parse_err(r#""a".lines().map(empty)"#), @r#"
3398 --> 1:17
3399 |
3400 1 | "a".lines().map(empty)
3401 | ^---^
3402 |
3403 = Expected lambda expression
3404 "#);
3405 insta::assert_snapshot!(env.parse_err(r#""a".lines().map(|| "")"#), @r#"
3407 --> 1:18
3408 |
3409 1 | "a".lines().map(|| "")
3410 | ^
3411 |
3412 = Expected 1 lambda parameters
3413 "#);
3414 insta::assert_snapshot!(env.parse_err(r#""a".lines().map(|a, b| "")"#), @r#"
3415 --> 1:18
3416 |
3417 1 | "a".lines().map(|a, b| "")
3418 | ^--^
3419 |
3420 = Expected 1 lambda parameters
3421 "#);
3422 insta::assert_snapshot!(env.parse_err(r#""a".lines().filter(|s| s ++ "\n")"#), @r#"
3424 --> 1:24
3425 |
3426 1 | "a".lines().filter(|s| s ++ "\n")
3427 | ^-------^
3428 |
3429 = Expected expression of type `Boolean`, but actual type is `Template`
3430 "#);
3431
3432 insta::assert_snapshot!(env.parse_err(r#""a".lines().any(|s| s.len())"#), @r#"
3434 --> 1:21
3435 |
3436 1 | "a".lines().any(|s| s.len())
3437 | ^-----^
3438 |
3439 = Expected expression of type `Boolean`, but actual type is `Integer`
3440 "#);
3441 insta::assert_snapshot!(env.parse_err(r#""a".lines().all(|s| s ++ "x")"#), @r#"
3443 --> 1:21
3444 |
3445 1 | "a".lines().all(|s| s ++ "x")
3446 | ^------^
3447 |
3448 = Expected expression of type `Boolean`, but actual type is `Template`
3449 "#);
3450 insta::assert_snapshot!(env.parse_err(r#""a".lines().any(|| true)"#), @r#"
3452 --> 1:18
3453 |
3454 1 | "a".lines().any(|| true)
3455 | ^
3456 |
3457 = Expected 1 lambda parameters
3458 "#);
3459 insta::assert_snapshot!(env.parse_err(r#""a".lines().all(|a, b| true)"#), @r#"
3461 --> 1:18
3462 |
3463 1 | "a".lines().all(|a, b| true)
3464 | ^--^
3465 |
3466 = Expected 1 lambda parameters
3467 "#);
3468 insta::assert_snapshot!(env.parse_err(r#""a".lines().map(|s| s.unknown())"#), @r#"
3470 --> 1:23
3471 |
3472 1 | "a".lines().map(|s| s.unknown())
3473 | ^-----^
3474 |
3475 = Method `unknown` doesn't exist for type `String`
3476 "#);
3477 env.add_alias("too_many_params", "|x, y| x");
3479 insta::assert_snapshot!(env.parse_err(r#""a".lines().map(too_many_params)"#), @r#"
3480 --> 1:17
3481 |
3482 1 | "a".lines().map(too_many_params)
3483 | ^-------------^
3484 |
3485 = In alias `too_many_params`
3486 --> 1:2
3487 |
3488 1 | |x, y| x
3489 | ^--^
3490 |
3491 = Expected 1 lambda parameters
3492 "#);
3493
3494 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().first()"#), @"a");
3496 insta::assert_snapshot!(env.render_ok(r#""".lines().first()"#), @"<Error: List is empty>");
3497
3498 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().last()"#), @"c");
3500 insta::assert_snapshot!(env.render_ok(r#""".lines().last()"#), @"<Error: List is empty>");
3501
3502 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().get(0)"#), @"a");
3504 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().get(1)"#), @"b");
3505 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().get(2)"#), @"c");
3506 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().get(3)"#), @"<Error: Index 3 out of bounds>");
3507 insta::assert_snapshot!(env.render_ok(r#""".lines().get(0)"#), @"<Error: Index 0 out of bounds>");
3508
3509 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().reverse().join("|")"#), @"c|b|a");
3511 insta::assert_snapshot!(env.render_ok(r#""".lines().reverse().join("|")"#), @"");
3512
3513 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().skip(0).join("|")"#), @"a|b|c");
3515 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().skip(1).join("|")"#), @"b|c");
3516 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().skip(2).join("|")"#), @"c");
3517 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().skip(3).join("|")"#), @"");
3518 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().skip(10).join("|")"#), @"");
3519
3520 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().take(0).join("|")"#), @"");
3522 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().take(1).join("|")"#), @"a");
3523 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().take(2).join("|")"#), @"a|b");
3524 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().take(3).join("|")"#), @"a|b|c");
3525 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().take(10).join("|")"#), @"a|b|c");
3526
3527 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc\nd".lines().skip(1).take(2).join("|")"#), @"b|c");
3529 }
3530
3531 #[test]
3532 fn test_string_method() {
3533 let mut env = TestTemplateEnv::new();
3534 env.add_keyword("description", || literal("description 1".to_owned()));
3535 env.add_keyword("bad_string", || new_error_property::<String>("Bad"));
3536
3537 insta::assert_snapshot!(env.render_ok(r#""".len()"#), @"0");
3538 insta::assert_snapshot!(env.render_ok(r#""foo".len()"#), @"3");
3539 insta::assert_snapshot!(env.render_ok(r#""💩".len()"#), @"4");
3540
3541 insta::assert_snapshot!(env.render_ok(r#""fooo".contains("foo")"#), @"true");
3542 insta::assert_snapshot!(env.render_ok(r#""foo".contains("fooo")"#), @"false");
3543 insta::assert_snapshot!(env.render_ok(r#"description.contains("description")"#), @"true");
3544 insta::assert_snapshot!(
3545 env.render_ok(r#""description 123".contains(description.first_line())"#),
3546 @"true");
3547
3548 insta::assert_snapshot!(env.parse_err(r#""fa".starts_with(regex:'[a-f]o+')"#), @r#"
3550 --> 1:18
3551 |
3552 1 | "fa".starts_with(regex:'[a-f]o+')
3553 | ^-------------^
3554 |
3555 = String patterns may not be used as expression values
3556 "#);
3557
3558 insta::assert_snapshot!(env.render_ok(r#""foo".contains(bad_string)"#), @"<Error: Bad>");
3560 insta::assert_snapshot!(
3561 env.render_ok(r#""foo".contains("f" ++ bad_string) ++ "bar""#), @"<Error: Bad>bar");
3562 insta::assert_snapshot!(
3563 env.render_ok(r#""foo".contains(separate("o", "f", bad_string))"#), @"<Error: Bad>");
3564
3565 insta::assert_snapshot!(env.render_ok(r#""fooo".match(regex:'[a-f]o+')"#), @"fooo");
3566 insta::assert_snapshot!(env.render_ok(r#""fa".match(regex:'[a-f]o+')"#), @"");
3567 insta::assert_snapshot!(env.render_ok(r#""hello".match(regex:"h(ell)o")"#), @"hello");
3568 insta::assert_snapshot!(env.render_ok(r#""HEllo".match(regex-i:"h(ell)o")"#), @"HEllo");
3569 insta::assert_snapshot!(env.render_ok(r#""hEllo".match(glob:"h*o")"#), @"hEllo");
3570 insta::assert_snapshot!(env.render_ok(r#""Hello".match(glob:"h*o")"#), @"");
3571 insta::assert_snapshot!(env.render_ok(r#""HEllo".match(glob-i:"h*o")"#), @"HEllo");
3572 insta::assert_snapshot!(env.render_ok(r#""hello".match("he")"#), @"he");
3573 insta::assert_snapshot!(env.render_ok(r#""hello".match(substring:"he")"#), @"he");
3574 insta::assert_snapshot!(env.render_ok(r#""hello".match(exact:"he")"#), @"");
3575
3576 insta::assert_snapshot!(env.render_ok(r#""🥺".match(regex:'(?-u)^(?:.)')"#), @"<Error: incomplete utf-8 byte sequence from index 0>");
3580
3581 insta::assert_snapshot!(env.parse_err(r#""hello".match(false)"#), @r#"
3582 --> 1:15
3583 |
3584 1 | "hello".match(false)
3585 | ^---^
3586 |
3587 = Expected string pattern
3588 "#);
3589 insta::assert_snapshot!(env.parse_err(r#""🥺".match(not-a-pattern:"abc")"#), @r#"
3590 --> 1:11
3591 |
3592 1 | "🥺".match(not-a-pattern:"abc")
3593 | ^-----------------^
3594 |
3595 = Bad string pattern
3596 Invalid string pattern kind `not-a-pattern:`
3597 "#);
3598
3599 insta::assert_snapshot!(env.render_ok(r#""".first_line()"#), @"");
3600 insta::assert_snapshot!(env.render_ok(r#""foo\nbar".first_line()"#), @"foo");
3601
3602 insta::assert_snapshot!(env.render_ok(r#""".lines()"#), @"");
3603 insta::assert_snapshot!(env.render_ok(r#""a\nb\nc\n".lines()"#), @"a b c");
3604
3605 insta::assert_snapshot!(env.render_ok(r#""".split(",")"#), @"");
3606 insta::assert_snapshot!(env.render_ok(r#""a,b,c".split(",")"#), @"a b c");
3607 insta::assert_snapshot!(env.render_ok(r#""a::b::c::d".split("::")"#), @"a b c d");
3608 insta::assert_snapshot!(env.render_ok(r#""a,b,c,d".split(",", 0)"#), @"");
3609 insta::assert_snapshot!(env.render_ok(r#""a,b,c,d".split(",", 2)"#), @"a b,c,d");
3610 insta::assert_snapshot!(env.render_ok(r#""a,b,c,d".split(",", 3)"#), @"a b c,d");
3611 insta::assert_snapshot!(env.render_ok(r#""a,b,c,d".split(",", 10)"#), @"a b c d");
3612 insta::assert_snapshot!(env.render_ok(r#""abc".split(",", -1)"#), @"<Error: out of range integral type conversion attempted>");
3613 insta::assert_snapshot!(env.render_ok(r#"json("a1b2c3".split(regex:'\d+'))"#), @r#"["a","b","c",""]"#);
3614 insta::assert_snapshot!(env.render_ok(r#""foo bar baz".split(regex:'\s+')"#), @"foo bar baz");
3615 insta::assert_snapshot!(env.render_ok(r#""a1b2c3d4".split(regex:'\d+', 3)"#), @"a b c3d4");
3616 insta::assert_snapshot!(env.render_ok(r#"json("hello world".split(regex-i:"WORLD"))"#), @r#"["hello ",""]"#);
3617
3618 insta::assert_snapshot!(env.render_ok("''.upper()"), @"");
3619 insta::assert_snapshot!(env.render_ok("'ABCabc 123!@#'.upper()"), @"ABCABC 123!@#");
3620 insta::assert_snapshot!(env.render_ok("''.lower()"), @"");
3621 insta::assert_snapshot!(env.render_ok("'ABCabc 123!@#'.lower()"), @"abcabc 123!@#");
3622
3623 insta::assert_snapshot!(env.render_ok(r#""".starts_with("")"#), @"true");
3624 insta::assert_snapshot!(env.render_ok(r#""everything".starts_with("")"#), @"true");
3625 insta::assert_snapshot!(env.render_ok(r#""".starts_with("foo")"#), @"false");
3626 insta::assert_snapshot!(env.render_ok(r#""foo".starts_with("foo")"#), @"true");
3627 insta::assert_snapshot!(env.render_ok(r#""foobar".starts_with("foo")"#), @"true");
3628 insta::assert_snapshot!(env.render_ok(r#""foobar".starts_with("bar")"#), @"false");
3629
3630 insta::assert_snapshot!(env.render_ok(r#""".ends_with("")"#), @"true");
3631 insta::assert_snapshot!(env.render_ok(r#""everything".ends_with("")"#), @"true");
3632 insta::assert_snapshot!(env.render_ok(r#""".ends_with("foo")"#), @"false");
3633 insta::assert_snapshot!(env.render_ok(r#""foo".ends_with("foo")"#), @"true");
3634 insta::assert_snapshot!(env.render_ok(r#""foobar".ends_with("foo")"#), @"false");
3635 insta::assert_snapshot!(env.render_ok(r#""foobar".ends_with("bar")"#), @"true");
3636
3637 insta::assert_snapshot!(env.render_ok(r#""".remove_prefix("wip: ")"#), @"");
3638 insta::assert_snapshot!(
3639 env.render_ok(r#""wip: testing".remove_prefix("wip: ")"#),
3640 @"testing");
3641
3642 insta::assert_snapshot!(
3643 env.render_ok(r#""bar@my.example.com".remove_suffix("@other.example.com")"#),
3644 @"bar@my.example.com");
3645 insta::assert_snapshot!(
3646 env.render_ok(r#""bar@other.example.com".remove_suffix("@other.example.com")"#),
3647 @"bar");
3648
3649 insta::assert_snapshot!(env.render_ok(r#"" \n \r \t \r ".trim()"#), @"");
3650 insta::assert_snapshot!(env.render_ok(r#"" \n \r foo bar \t \r ".trim()"#), @"foo bar");
3651
3652 insta::assert_snapshot!(env.render_ok(r#"" \n \r \t \r ".trim_start()"#), @"");
3653 insta::assert_snapshot!(env.render_ok(r#"" \n \r foo bar \t \r ".trim_start()"#), @"foo bar");
3654
3655 insta::assert_snapshot!(env.render_ok(r#"" \n \r \t \r ".trim_end()"#), @"");
3656 insta::assert_snapshot!(env.render_ok(r#"" \n \r foo bar \t \r ".trim_end()"#), @"\n\r foo bar");
3657
3658 insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 0)"#), @"");
3659 insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 1)"#), @"f");
3660 insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 3)"#), @"foo");
3661 insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 4)"#), @"foo");
3662 insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(2, -1)"#), @"cde");
3663 insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-3, 99)"#), @"def");
3664 insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-6, 99)"#), @"abcdef");
3665 insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-7, 1)"#), @"a");
3666
3667 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(2, -1)"#), @"c💩");
3669 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, -3)"#), @"💩");
3670 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, -4)"#), @"");
3671 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(6, -3)"#), @"💩");
3672 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(7, -3)"#), @"");
3673 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, 4)"#), @"");
3674 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, 6)"#), @"");
3675 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, 7)"#), @"💩");
3676 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-1, 7)"#), @"");
3677 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-3, 7)"#), @"");
3678 insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-4, 7)"#), @"💩");
3679
3680 insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(4, 2)"#), @"");
3682 insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-2, -4)"#), @"");
3683
3684 insta::assert_snapshot!(env.render_ok(r#""hello".escape_json()"#), @r#""hello""#);
3685 insta::assert_snapshot!(env.render_ok(r#""he \n ll \n \" o".escape_json()"#), @r#""he \n ll \n \" o""#);
3686
3687 insta::assert_snapshot!(env.render_ok(r#""hello world".replace("world", "jj")"#), @"hello jj");
3689 insta::assert_snapshot!(env.render_ok(r#""hello world world".replace("world", "jj")"#), @"hello jj jj");
3690 insta::assert_snapshot!(env.render_ok(r#""hello".replace("missing", "jj")"#), @"hello");
3691
3692 insta::assert_snapshot!(env.render_ok(r#""hello world world".replace("world", "jj", 0)"#), @"hello world world");
3694 insta::assert_snapshot!(env.render_ok(r#""hello world world".replace("world", "jj", 1)"#), @"hello jj world");
3695 insta::assert_snapshot!(env.render_ok(r#""hello world world world".replace("world", "jj", 2)"#), @"hello jj jj world");
3696
3697 insta::assert_snapshot!(env.render_ok(r#""hello world world".replace("world", "jj", -1)"#), @"<Error: out of range integral type conversion attempted>");
3699 insta::assert_snapshot!(env.render_ok(r#""hello world world".replace("world", "jj", -5)"#), @"<Error: out of range integral type conversion attempted>");
3700
3701 insta::assert_snapshot!(env.render_ok(r#""hello123world456".replace(regex:'\d+', "X")"#), @"helloXworldX");
3703 insta::assert_snapshot!(env.render_ok(r#""hello123world456".replace(regex:'\d+', "X", 1)"#), @"helloXworld456");
3704
3705 insta::assert_snapshot!(env.render_ok(r#""HELLO WORLD".replace(regex-i:"(hello) +(world)", "$2 $1")"#), @"WORLD HELLO");
3707 insta::assert_snapshot!(env.render_ok(r#""abc123".replace(regex:"([a-z]+)([0-9]+)", "$2-$1")"#), @"123-abc");
3708 insta::assert_snapshot!(env.render_ok(r#""foo123bar".replace(regex:'\d+', "[$0]")"#), @"foo[123]bar");
3709
3710 insta::assert_snapshot!(env.render_ok(r#""Hello World".replace(regex-i:"hello", "hi")"#), @"hi World");
3712 insta::assert_snapshot!(env.render_ok(r#""Hello World Hello".replace(regex-i:"hello", "hi")"#), @"hi World hi");
3713 insta::assert_snapshot!(env.render_ok(r#""Hello World Hello".replace(regex-i:"hello", "hi", 1)"#), @"hi World Hello");
3714
3715 insta::assert_snapshot!(env.render_ok(r#"'hello\d+world'.replace('\d+', "X")"#), @"helloXworld");
3717 insta::assert_snapshot!(env.render_ok(r#""(foo)($1)bar".replace("$1", "$2")"#), @"(foo)()bar");
3718 insta::assert_snapshot!(env.render_ok(r#""test(abc)end".replace("(abc)", "X")"#), @"testXend");
3719
3720 insta::assert_snapshot!(env.render_ok(r#""hello world".replace("world", description.first_line())"#), @"hello description 1");
3722
3723 insta::assert_snapshot!(env.render_ok(r#""hello world".replace("world", bad_string)"#), @"<Error: Bad>");
3725 }
3726
3727 #[test]
3728 fn test_config_value_method() {
3729 let mut env = TestTemplateEnv::new();
3730 env.add_keyword("boolean", || literal(ConfigValue::from(true)));
3731 env.add_keyword("integer", || literal(ConfigValue::from(42)));
3732 env.add_keyword("string", || literal(ConfigValue::from("foo")));
3733 env.add_keyword("string_list", || {
3734 literal(ConfigValue::from_iter(["foo", "bar"]))
3735 });
3736
3737 insta::assert_snapshot!(env.render_ok("boolean"), @"true");
3738 insta::assert_snapshot!(env.render_ok("integer"), @"42");
3739 insta::assert_snapshot!(env.render_ok("string"), @r#""foo""#);
3740 insta::assert_snapshot!(env.render_ok("string_list"), @r#"["foo", "bar"]"#);
3741
3742 insta::assert_snapshot!(env.render_ok("boolean.as_boolean()"), @"true");
3743 insta::assert_snapshot!(env.render_ok("integer.as_integer()"), @"42");
3744 insta::assert_snapshot!(env.render_ok("string.as_string()"), @"foo");
3745 insta::assert_snapshot!(env.render_ok("string_list.as_string_list()"), @"foo bar");
3746
3747 insta::assert_snapshot!(
3748 env.render_ok("boolean.as_integer()"),
3749 @"<Error: invalid type: boolean `true`, expected i64>");
3750 insta::assert_snapshot!(
3751 env.render_ok("integer.as_string()"),
3752 @"<Error: invalid type: integer `42`, expected a string>");
3753 insta::assert_snapshot!(
3754 env.render_ok("string.as_string_list()"),
3755 @r#"<Error: invalid type: string "foo", expected a sequence>"#);
3756 insta::assert_snapshot!(
3757 env.render_ok("string_list.as_boolean()"),
3758 @"<Error: invalid type: sequence, expected a boolean>");
3759 }
3760
3761 #[test]
3762 fn test_signature_and_email_methods() {
3763 let mut env = TestTemplateEnv::new();
3764
3765 env.add_keyword("author", || {
3766 literal(new_signature("Test User", "test.user@example.com"))
3767 });
3768 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user@example.com>");
3769 insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Test User");
3770 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@example.com");
3771 insta::assert_snapshot!(env.render_ok("author.email().local()"), @"test.user");
3772 insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"example.com");
3773 insta::assert_snapshot!(env.render_ok("author.timestamp()"), @"1970-01-01 00:00:00.000 +00:00");
3774
3775 env.add_keyword("author", || {
3776 literal(new_signature("Another Test User", "test.user@example.com"))
3777 });
3778 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Another Test User <test.user@example.com>");
3779 insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Another Test User");
3780 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@example.com");
3781
3782 env.add_keyword("author", || {
3783 literal(new_signature("Test User", "test.user@invalid@example.com"))
3784 });
3785 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user@invalid@example.com>");
3786 insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Test User");
3787 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@invalid@example.com");
3788 insta::assert_snapshot!(env.render_ok("author.email().local()"), @"test.user");
3789 insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"invalid@example.com");
3790
3791 env.add_keyword("author", || {
3792 literal(new_signature("Test User", "test.user"))
3793 });
3794 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user>");
3795 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user");
3796 insta::assert_snapshot!(env.render_ok("author.email().local()"), @"test.user");
3797 insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"");
3798
3799 env.add_keyword("author", || {
3800 literal(new_signature("Test User", "test.user+tag@example.com"))
3801 });
3802 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user+tag@example.com>");
3803 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user+tag@example.com");
3804 insta::assert_snapshot!(env.render_ok("author.email().local()"), @"test.user+tag");
3805 insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"example.com");
3806
3807 env.add_keyword("author", || literal(new_signature("Test User", "x@y")));
3808 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <x@y>");
3809 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"x@y");
3810 insta::assert_snapshot!(env.render_ok("author.email().local()"), @"x");
3811 insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"y");
3812
3813 env.add_keyword("author", || {
3814 literal(new_signature("", "test.user@example.com"))
3815 });
3816 insta::assert_snapshot!(env.render_ok(r#"author"#), @"<test.user@example.com>");
3817 insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"");
3818 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@example.com");
3819
3820 env.add_keyword("author", || literal(new_signature("Test User", "")));
3821 insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User");
3822 insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Test User");
3823 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"");
3824 insta::assert_snapshot!(env.render_ok("author.email().local()"), @"");
3825 insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"");
3826
3827 env.add_keyword("author", || literal(new_signature("", "")));
3828 insta::assert_snapshot!(env.render_ok(r#"author"#), @"");
3829 insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"");
3830 insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"");
3831 }
3832
3833 #[test]
3834 fn test_size_hint_method() {
3835 let mut env = TestTemplateEnv::new();
3836
3837 env.add_keyword("unbounded", || literal((5, None)));
3838 insta::assert_snapshot!(env.render_ok(r#"unbounded.lower()"#), @"5");
3839 insta::assert_snapshot!(env.render_ok(r#"unbounded.upper()"#), @"");
3840 insta::assert_snapshot!(env.render_ok(r#"unbounded.exact()"#), @"");
3841 insta::assert_snapshot!(env.render_ok(r#"unbounded.zero()"#), @"false");
3842
3843 env.add_keyword("bounded", || literal((0, Some(10))));
3844 insta::assert_snapshot!(env.render_ok(r#"bounded.lower()"#), @"0");
3845 insta::assert_snapshot!(env.render_ok(r#"bounded.upper()"#), @"10");
3846 insta::assert_snapshot!(env.render_ok(r#"bounded.exact()"#), @"");
3847 insta::assert_snapshot!(env.render_ok(r#"bounded.zero()"#), @"false");
3848
3849 env.add_keyword("zero", || literal((0, Some(0))));
3850 insta::assert_snapshot!(env.render_ok(r#"zero.lower()"#), @"0");
3851 insta::assert_snapshot!(env.render_ok(r#"zero.upper()"#), @"0");
3852 insta::assert_snapshot!(env.render_ok(r#"zero.exact()"#), @"0");
3853 insta::assert_snapshot!(env.render_ok(r#"zero.zero()"#), @"true");
3854 }
3855
3856 #[test]
3857 fn test_timestamp_method() {
3858 let mut env = TestTemplateEnv::new();
3859 env.add_keyword("now", || literal(Timestamp::now()));
3860 env.add_keyword("t0", || literal(new_timestamp(0, 0)));
3861 env.add_keyword("t0_plus1", || literal(new_timestamp(0, 60)));
3862 env.add_keyword("tmax", || literal(new_timestamp(i64::MAX, 0)));
3863
3864 insta::assert_snapshot!(env.render_ok("tmax"),
3866 @"<Error: Out-of-range date>");
3867
3868 insta::assert_snapshot!(
3869 env.render_ok(r#"t0.format("%Y%m%d %H:%M:%S")"#),
3870 @"19700101 00:00:00");
3871
3872 insta::assert_snapshot!(env.parse_err(r#"t0.format("%_")"#), @r#"
3874 --> 1:11
3875 |
3876 1 | t0.format("%_")
3877 | ^--^
3878 |
3879 = Invalid time format
3880 "#);
3881
3882 insta::assert_snapshot!(env.parse_err(r#"t0.format(0)"#), @"
3884 --> 1:11
3885 |
3886 1 | t0.format(0)
3887 | ^
3888 |
3889 = Expected string literal
3890 ");
3891
3892 insta::assert_snapshot!(env.parse_err(r#"t0.format("%Y" ++ "%m")"#), @r#"
3894 --> 1:11
3895 |
3896 1 | t0.format("%Y" ++ "%m")
3897 | ^----------^
3898 |
3899 = Expected string literal
3900 "#);
3901
3902 env.add_alias("time_format", r#""%Y-%m-%d""#);
3904 env.add_alias("bad_time_format", r#""%_""#);
3905 insta::assert_snapshot!(env.render_ok(r#"t0.format(time_format)"#), @"1970-01-01");
3906 insta::assert_snapshot!(env.parse_err(r#"t0.format(bad_time_format)"#), @r#"
3907 --> 1:11
3908 |
3909 1 | t0.format(bad_time_format)
3910 | ^-------------^
3911 |
3912 = In alias `bad_time_format`
3913 --> 1:1
3914 |
3915 1 | "%_"
3916 | ^--^
3917 |
3918 = Invalid time format
3919 "#);
3920
3921 insta::assert_snapshot!(env.render_ok("t0_plus1.utc()"), @"1970-01-01 00:00:00.000 +00:00");
3922
3923 assert!(!env.render_ok("now.ago()").is_empty());
3926 assert!(!env.render_ok("now.local()").is_empty());
3927
3928 insta::assert_snapshot!(env.render_ok("t0.after('1969')"), @"true");
3929 insta::assert_snapshot!(env.render_ok("t0.before('1969')"), @"false");
3930 insta::assert_snapshot!(env.render_ok("t0.after('now')"), @"false");
3931 insta::assert_snapshot!(env.render_ok("t0.before('now')"), @"true");
3932 insta::assert_snapshot!(env.parse_err("t0.before('invalid')"), @"
3933 --> 1:11
3934 |
3935 1 | t0.before('invalid')
3936 | ^-------^
3937 |
3938 = Invalid date pattern
3939 expected unsupported identifier as position 0..7
3940 ");
3941 insta::assert_snapshot!(env.parse_err("t0.before('invalid')"), @"
3942 --> 1:11
3943 |
3944 1 | t0.before('invalid')
3945 | ^-------^
3946 |
3947 = Invalid date pattern
3948 expected unsupported identifier as position 0..7
3949 ");
3950
3951 insta::assert_snapshot!(env.parse_err("t0.after(t0)"), @"
3953 --> 1:10
3954 |
3955 1 | t0.after(t0)
3956 | ^^
3957 |
3958 = Expected string literal
3959 ");
3960 insta::assert_snapshot!(env.parse_err("t0.before(t0)"), @"
3961 --> 1:11
3962 |
3963 1 | t0.before(t0)
3964 | ^^
3965 |
3966 = Expected string literal
3967 ");
3968
3969 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");
3970 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");
3971 insta::assert_snapshot!(env.parse_err("t0.since(false)"), @"
3972 --> 1:10
3973 |
3974 1 | t0.since(false)
3975 | ^---^
3976 |
3977 = Expected expression of type `Timestamp`, but actual type is `Boolean`
3978 ");
3979 }
3980
3981 #[test]
3982 fn test_timestamp_range_method() {
3983 let mut env = TestTemplateEnv::new();
3984 env.add_keyword("instant", || {
3985 literal(TimestampRange {
3986 start: new_timestamp(0, 0),
3987 end: new_timestamp(0, 0),
3988 })
3989 });
3990 env.add_keyword("one_msec", || {
3991 literal(TimestampRange {
3992 start: new_timestamp(0, 0),
3993 end: new_timestamp(1, -60),
3994 })
3995 });
3996
3997 insta::assert_snapshot!(
3998 env.render_ok("instant.start().format('%Y%m%d %H:%M:%S %Z')"),
3999 @"19700101 00:00:00 +00:00");
4000 insta::assert_snapshot!(
4001 env.render_ok("one_msec.end().format('%Y%m%d %H:%M:%S %Z')"),
4002 @"19691231 23:00:00 -01:00");
4003
4004 insta::assert_snapshot!(
4005 env.render_ok("instant.duration()"), @"less than a microsecond");
4006 insta::assert_snapshot!(
4007 env.render_ok("one_msec.duration()"), @"1 millisecond");
4008 }
4009
4010 #[test]
4011 fn test_fill_function() {
4012 let mut env = TestTemplateEnv::new();
4013 env.add_color("error", crossterm::style::Color::DarkRed);
4014
4015 insta::assert_snapshot!(
4016 env.render_ok(r#"fill(20, "The quick fox jumps over the " ++
4017 label("error", "lazy") ++ " dog\n")"#),
4018 @"
4019 The quick fox jumps
4020 over the [38;5;1mlazy[39m dog
4021 ");
4022
4023 insta::assert_snapshot!(
4025 env.render_ok(r#"fill(9, "Longlonglongword an some short words " ++
4026 label("error", "longlonglongword and short words") ++
4027 " back out\n")"#),
4028 @"
4029 Longlonglongword
4030 an some
4031 short
4032 words
4033 [38;5;1mlonglonglongword[39m
4034 [38;5;1mand short[39m
4035 [38;5;1mwords[39m
4036 back out
4037 ");
4038
4039 insta::assert_snapshot!(
4041 env.render_ok(r#"fill(0, "The quick fox jumps over the " ++
4042 label("error", "lazy") ++ " dog\n")"#),
4043 @"
4044 The
4045 quick
4046 fox
4047 jumps
4048 over
4049 the
4050 [38;5;1mlazy[39m
4051 dog
4052 ");
4053
4054 insta::assert_snapshot!(
4056 env.render_ok(r#"fill(-0, "The quick fox jumps over the " ++
4057 label("error", "lazy") ++ " dog\n")"#),
4058 @"
4059 The
4060 quick
4061 fox
4062 jumps
4063 over
4064 the
4065 [38;5;1mlazy[39m
4066 dog
4067 ");
4068
4069 insta::assert_snapshot!(
4071 env.render_ok(r#"fill(-10, "The quick fox jumps over the " ++
4072 label("error", "lazy") ++ " dog\n")"#),
4073 @"[38;5;1m<Error: out of range integral type conversion attempted>[39m");
4074
4075 insta::assert_snapshot!(
4077 env.render_ok(r#""START marker to help insta\n" ++
4078 indent(" ", fill(20, "The quick fox jumps over the " ++
4079 label("error", "lazy") ++ " dog\n"))"#),
4080 @"
4081 START marker to help insta
4082 The quick fox jumps
4083 over the [38;5;1mlazy[39m dog
4084 ");
4085
4086 insta::assert_snapshot!(
4088 env.render_ok(r#""START marker to help insta\n" ++
4089 fill(20, indent(" ", "The quick fox jumps over the " ++
4090 label("error", "lazy") ++ " dog\n"))"#),
4091 @"
4092 START marker to help insta
4093 The quick fox
4094 jumps over the [38;5;1mlazy[39m
4095 dog
4096 ");
4097 }
4098
4099 #[test]
4100 fn test_indent_function() {
4101 let mut env = TestTemplateEnv::new();
4102 env.add_color("error", crossterm::style::Color::DarkRed);
4103 env.add_color("warning", crossterm::style::Color::DarkYellow);
4104 env.add_color("hint", crossterm::style::Color::DarkCyan);
4105
4106 assert_eq!(env.render_ok(r#"indent("__", "")"#), "");
4109 assert_eq!(env.render_ok(r#"indent("__", "\n")"#), "\n");
4110 assert_eq!(env.render_ok(r#"indent("__", "a\n\nb")"#), "__a\n\n__b");
4111
4112 insta::assert_snapshot!(
4114 env.render_ok(r#"indent("__", label("error", "a\n") ++ label("warning", "b\n"))"#),
4115 @"
4116 [38;5;1m__a[39m
4117 [38;5;3m__b[39m
4118 ");
4119
4120 insta::assert_snapshot!(
4122 env.render_ok(r#"indent("__", label("error", "a") ++ label("warning", "b\nc"))"#),
4123 @"
4124 [38;5;1m__a[38;5;3mb[39m
4125 [38;5;3m__c[39m
4126 ");
4127
4128 insta::assert_snapshot!(
4130 env.render_ok(r#"indent(label("error", "XX"), "a\nb\n")"#),
4131 @"
4132 [38;5;1mXX[39ma
4133 [38;5;1mXX[39mb
4134 ");
4135
4136 insta::assert_snapshot!(
4138 env.render_ok(r#"indent(label("hint", "A"),
4139 label("warning", indent(label("hint", "B"),
4140 label("error", "x\n") ++ "y")))"#),
4141 @"
4142 [38;5;6mAB[38;5;1mx[39m
4143 [38;5;6mAB[38;5;3my[39m
4144 ");
4145 }
4146
4147 #[test]
4148 fn test_pad_function() {
4149 let mut env = TestTemplateEnv::new();
4150 env.add_keyword("bad_string", || new_error_property::<String>("Bad"));
4151 env.add_color("red", crossterm::style::Color::Red);
4152 env.add_color("cyan", crossterm::style::Color::DarkCyan);
4153
4154 insta::assert_snapshot!(
4156 env.render_ok(r"'{' ++ pad_start(5, label('red', 'foo')) ++ '}'"),
4157 @"{ [38;5;9mfoo[39m}");
4158 insta::assert_snapshot!(
4159 env.render_ok(r"'{' ++ pad_end(5, label('red', 'foo')) ++ '}'"),
4160 @"{[38;5;9mfoo[39m }");
4161 insta::assert_snapshot!(
4162 env.render_ok(r"'{' ++ pad_centered(5, label('red', 'foo')) ++ '}'"),
4163 @"{ [38;5;9mfoo[39m }");
4164
4165 insta::assert_snapshot!(
4167 env.render_ok(r"pad_start(5, label('red', 'foo'), fill_char=label('cyan', '='))"),
4168 @"[38;5;6m==[38;5;9mfoo[39m");
4169 insta::assert_snapshot!(
4170 env.render_ok(r"pad_end(5, label('red', 'foo'), fill_char=label('cyan', '='))"),
4171 @"[38;5;9mfoo[38;5;6m==[39m");
4172 insta::assert_snapshot!(
4173 env.render_ok(r"pad_centered(5, label('red', 'foo'), fill_char=label('cyan', '='))"),
4174 @"[38;5;6m=[38;5;9mfoo[38;5;6m=[39m");
4175
4176 insta::assert_snapshot!(
4179 env.render_ok(r"pad_start(3, 'foo', fill_char=bad_string)"),
4180 @"foo");
4181 insta::assert_snapshot!(
4182 env.render_ok(r"pad_end(5, 'foo', fill_char=bad_string)"),
4183 @"foo<<Error: Error: Bad>Bad>");
4184 insta::assert_snapshot!(
4185 env.render_ok(r"pad_centered(5, 'foo', fill_char=bad_string)"),
4186 @"<Error: Bad>foo<Error: Bad>");
4187
4188 insta::assert_snapshot!(
4190 env.render_ok("pad_start(-1, 'foo')"),
4191 @"<Error: out of range integral type conversion attempted>");
4192 }
4193
4194 #[test]
4195 fn test_hash_function() {
4196 let mut env = TestTemplateEnv::new();
4197 env.add_color("red", crossterm::style::Color::Red);
4198
4199 assert_eq!(env.render_ok("hash(false)"), env.render_ok("hash('false')"));
4202 assert_eq!(env.render_ok("hash(0)"), env.render_ok("hash('0')"));
4203 assert_eq!(
4204 env.render_ok("hash(0)"),
4205 env.render_ok("hash(label('red', '0'))")
4206 );
4207 }
4208
4209 #[test]
4210 fn test_truncate_function() {
4211 let mut env = TestTemplateEnv::new();
4212 env.add_color("red", crossterm::style::Color::Red);
4213
4214 insta::assert_snapshot!(
4215 env.render_ok(r"truncate_start(2, label('red', 'foobar')) ++ 'baz'"),
4216 @"[38;5;9mar[39mbaz");
4217 insta::assert_snapshot!(
4218 env.render_ok("truncate_start(5, 'foo', 'bar')"), @"foo");
4219 insta::assert_snapshot!(
4220 env.render_ok("truncate_start(9, 'foobarbazquux', 'dotdot')"), @"dotdotuux");
4221
4222 insta::assert_snapshot!(
4223 env.render_ok(r"truncate_end(2, label('red', 'foobar')) ++ 'baz'"),
4224 @"[38;5;9mfo[39mbaz");
4225 insta::assert_snapshot!(
4226 env.render_ok("truncate_end(5, 'foo', 'bar')"), @"foo");
4227 insta::assert_snapshot!(
4228 env.render_ok("truncate_end(9, 'foobarbazquux', 'dotdot')"), @"foodotdot");
4229
4230 insta::assert_snapshot!(
4232 env.render_ok("truncate_end(-1, 'foo')"),
4233 @"<Error: out of range integral type conversion attempted>");
4234 }
4235
4236 #[test]
4237 fn test_label_function() {
4238 let mut env = TestTemplateEnv::new();
4239 env.add_keyword("empty", || literal(true));
4240 env.add_color("error", crossterm::style::Color::DarkRed);
4241 env.add_color("warning", crossterm::style::Color::DarkYellow);
4242
4243 insta::assert_snapshot!(
4245 env.render_ok(r#"label("error", "text")"#),
4246 @"[38;5;1mtext[39m");
4247
4248 insta::assert_snapshot!(
4250 env.render_ok(r#"label("error".first_line(), "text")"#),
4251 @"[38;5;1mtext[39m");
4252
4253 insta::assert_snapshot!(
4255 env.render_ok("label(fill(-1, 'foo'), 'text')"),
4256 @"[38;5;1m<Error: out of range integral type conversion attempted>[39m");
4257
4258 insta::assert_snapshot!(
4260 env.render_ok(r#"label(if(empty, "error", "warning"), "text")"#),
4261 @"[38;5;1mtext[39m");
4262 }
4263
4264 #[test]
4265 fn test_raw_escape_sequence_function_strip_labels() {
4266 let mut env = TestTemplateEnv::new();
4267 env.add_color("error", crossterm::style::Color::DarkRed);
4268 env.add_color("warning", crossterm::style::Color::DarkYellow);
4269
4270 insta::assert_snapshot!(
4271 env.render_ok(r#"raw_escape_sequence(label("error warning", "text"))"#),
4272 @"text",
4273 );
4274 }
4275
4276 #[test]
4277 fn test_raw_escape_sequence_function_ansi_escape() {
4278 let env = TestTemplateEnv::new();
4279
4280 insta::assert_snapshot!(env.render_ok(r#""\e""#), @"␛");
4282 insta::assert_snapshot!(env.render_ok(r#""\x1b""#), @"␛");
4283 insta::assert_snapshot!(env.render_ok(r#""\x1B""#), @"␛");
4284 insta::assert_snapshot!(
4285 env.render_ok(r#""]8;;"
4286 ++ "http://example.com"
4287 ++ "\e\\"
4288 ++ "Example"
4289 ++ "\x1b]8;;\x1B\\""#),
4290 @r"␛]8;;http://example.com␛\Example␛]8;;␛\");
4291
4292 insta::assert_snapshot!(env.render_ok(r#"raw_escape_sequence("\e")"#), @"");
4294 insta::assert_snapshot!(env.render_ok(r#"raw_escape_sequence("\x1b")"#), @"");
4295 insta::assert_snapshot!(env.render_ok(r#"raw_escape_sequence("\x1B")"#), @"");
4296 insta::assert_snapshot!(
4297 env.render_ok(r#"raw_escape_sequence("]8;;"
4298 ++ "http://example.com"
4299 ++ "\e\\"
4300 ++ "Example"
4301 ++ "\x1b]8;;\x1B\\")"#),
4302 @r"]8;;http://example.com\Example]8;;\");
4303 }
4304
4305 #[test]
4306 fn test_hyperlink_function_with_color() {
4307 let mut env = TestTemplateEnv::new();
4308 env.add_keyword("bad_string", || new_error_property::<String>("Bad"));
4309 insta::assert_snapshot!(
4311 env.render_ok(r#"hyperlink("http://example.com", "Example")"#),
4312 @r"]8;;http://example.com\Example]8;;\");
4313 insta::assert_snapshot!(
4314 env.render_ok(r#"hyperlink(bad_string, "Example")"#),
4315 @"<Error: Bad>");
4316 }
4317
4318 #[test]
4319 fn test_hyperlink_function_without_color() {
4320 let env = TestTemplateEnv::new();
4321 insta::assert_snapshot!(
4323 env.render_plain(r#"hyperlink("http://example.com", "Example")"#),
4324 @"Example");
4325 }
4326
4327 #[test]
4328 fn test_hyperlink_function_custom_fallback() {
4329 let env = TestTemplateEnv::new();
4330 insta::assert_snapshot!(
4332 env.render_plain(r#"hyperlink("http://example.com", "Example", "URL: http://example.com")"#),
4333 @"URL: http://example.com");
4334 }
4335
4336 #[test]
4337 fn test_hyperlink_function_stringify() {
4338 let env = TestTemplateEnv::new();
4339 insta::assert_snapshot!(
4341 env.render_ok(r#"stringify(hyperlink("http://example.com", "Example"))"#),
4342 @"Example");
4343 insta::assert_snapshot!(
4345 env.render_ok(r#"stringify(hyperlink("http://example.com", "Example")).upper()"#),
4346 @"EXAMPLE");
4347 }
4348
4349 #[test]
4350 fn test_hyperlink_function_with_separate() {
4351 let env = TestTemplateEnv::new();
4352 insta::assert_snapshot!(
4354 env.render_ok(r#"separate(" | ", hyperlink("http://a.com", "A"), hyperlink("http://b.com", "B"))"#),
4355 @r"]8;;http://a.com\A]8;;\ | ]8;;http://b.com\B]8;;\");
4356 }
4357
4358 #[test]
4359 fn test_hyperlink_function_with_coalesce() {
4360 let env = TestTemplateEnv::new();
4361 insta::assert_snapshot!(
4363 env.render_ok(r#"coalesce(hyperlink("http://example.com", "Link"), "fallback")"#),
4364 @r"]8;;http://example.com\Link]8;;\");
4365 insta::assert_snapshot!(
4367 env.render_ok(r#"coalesce(hyperlink("http://example.com", ""), "fallback")"#),
4368 @"fallback");
4369 }
4370
4371 #[test]
4372 fn test_hyperlink_function_with_if() {
4373 let env = TestTemplateEnv::new();
4374 insta::assert_snapshot!(
4376 env.render_ok(r#"if(true, hyperlink("http://example.com", "Yes"), "No")"#),
4377 @r"]8;;http://example.com\Yes]8;;\");
4378 insta::assert_snapshot!(
4379 env.render_ok(r#"if(false, "Yes", hyperlink("http://example.com", "No"))"#),
4380 @r"]8;;http://example.com\No]8;;\");
4381 }
4382
4383 #[test]
4384 fn test_hyperlink_function_plain_with_separate() {
4385 let env = TestTemplateEnv::new();
4386 insta::assert_snapshot!(
4388 env.render_plain(r#"separate(" | ", hyperlink("http://a.com", "A"), hyperlink("http://b.com", "B"))"#),
4389 @"A | B");
4390 }
4391
4392 #[test]
4393 fn test_stringify_function() {
4394 let mut env = TestTemplateEnv::new();
4395 env.add_keyword("none_i64", || literal(None::<i64>));
4396 env.add_color("error", crossterm::style::Color::DarkRed);
4397
4398 insta::assert_snapshot!(env.render_ok("stringify(false)"), @"false");
4399 insta::assert_snapshot!(env.render_ok("stringify(42).len()"), @"2");
4400 insta::assert_snapshot!(env.render_ok("stringify(none_i64)"), @"");
4401 insta::assert_snapshot!(env.render_ok("stringify(label('error', 'text'))"), @"text");
4402 }
4403
4404 #[test]
4405 fn test_json_function() {
4406 let mut env = TestTemplateEnv::new();
4407 env.add_keyword("none_i64", || literal(None::<i64>));
4408 env.add_keyword("string_list", || {
4409 literal(vec!["foo".to_owned(), "bar".to_owned()])
4410 });
4411 env.add_keyword("config_value_table", || {
4412 literal(ConfigValue::from_iter([("foo", "bar")]))
4413 });
4414 env.add_keyword("some_cfgval", || literal(Some(ConfigValue::from(1))));
4415 env.add_keyword("none_cfgval", || literal(None::<ConfigValue>));
4416 env.add_keyword("signature", || {
4417 literal(Signature {
4418 name: "Test User".to_owned(),
4419 email: "test.user@example.com".to_owned(),
4420 timestamp: Timestamp {
4421 timestamp: MillisSinceEpoch(0),
4422 tz_offset: 0,
4423 },
4424 })
4425 });
4426 env.add_keyword("email", || literal(Email("foo@bar".to_owned())));
4427 env.add_keyword("size_hint", || literal((5, None)));
4428 env.add_keyword("timestamp", || {
4429 literal(Timestamp {
4430 timestamp: MillisSinceEpoch(0),
4431 tz_offset: 0,
4432 })
4433 });
4434 env.add_keyword("timestamp_range", || {
4435 literal(TimestampRange {
4436 start: Timestamp {
4437 timestamp: MillisSinceEpoch(0),
4438 tz_offset: 0,
4439 },
4440 end: Timestamp {
4441 timestamp: MillisSinceEpoch(86_400_000),
4442 tz_offset: -60,
4443 },
4444 })
4445 });
4446
4447 insta::assert_snapshot!(env.render_ok(r#"json('"quoted"')"#), @r#""\"quoted\"""#);
4448 insta::assert_snapshot!(env.render_ok(r#"json(string_list)"#), @r#"["foo","bar"]"#);
4449 insta::assert_snapshot!(env.render_ok("json(false)"), @"false");
4450 insta::assert_snapshot!(env.render_ok("json(42)"), @"42");
4451 insta::assert_snapshot!(env.render_ok("json(none_i64)"), @"null");
4452 insta::assert_snapshot!(env.render_ok(r#"json(config_value_table)"#), @r#"{"foo":"bar"}"#);
4453 insta::assert_snapshot!(env.render_ok(r"json(some_cfgval)"), @"1");
4454 insta::assert_snapshot!(env.render_ok(r"json(none_cfgval)"), @"null");
4455 insta::assert_snapshot!(env.render_ok("json(email)"), @r#""foo@bar""#);
4456 insta::assert_snapshot!(
4457 env.render_ok("json(signature)"),
4458 @r#"{"name":"Test User","email":"test.user@example.com","timestamp":"1970-01-01T00:00:00Z"}"#);
4459 insta::assert_snapshot!(env.render_ok("json(size_hint)"), @"[5,null]");
4460 insta::assert_snapshot!(env.render_ok("json(timestamp)"), @r#""1970-01-01T00:00:00Z""#);
4461 insta::assert_snapshot!(
4462 env.render_ok("json(timestamp_range)"),
4463 @r#"{"start":"1970-01-01T00:00:00Z","end":"1970-01-01T23:00:00-01:00"}"#);
4464
4465 insta::assert_snapshot!(env.render_ok(r#"json(string_list.map(|s| s))"#), @r#"["foo","bar"]"#);
4467 insta::assert_snapshot!(env.render_ok(r#"json(string_list.map(|s| size_hint))"#), @"[[5,null],[5,null]]");
4468
4469 insta::assert_snapshot!(env.render_ok(r#"json(if(true, email, timestamp))"#), @r#""foo@bar""#);
4471 insta::assert_snapshot!(env.render_ok(r#"json(if(true, size_hint, config_value_table))"#), @"[5,null]");
4472
4473 insta::assert_snapshot!(env.parse_err(r#"json(if(true, email))"#), @r###"
4476 --> 1:6
4477 |
4478 1 | json(if(true, email))
4479 | ^-------------^
4480 |
4481 = Expected expression of type `Serialize`, but actual type is `Any`
4482 "###);
4483 insta::assert_snapshot!(env.parse_err(r#"json(if(false, email))"#), @r###"
4484 --> 1:6
4485 |
4486 1 | json(if(false, email))
4487 | ^--------------^
4488 |
4489 = Expected expression of type `Serialize`, but actual type is `Any`
4490 "###);
4491 }
4492
4493 #[test]
4494 fn test_coalesce_function() {
4495 let mut env = TestTemplateEnv::new();
4496 env.add_keyword("bad_string", || new_error_property::<String>("Bad"));
4497 env.add_keyword("empty_string", || literal("".to_owned()));
4498 env.add_keyword("non_empty_string", || literal("a".to_owned()));
4499
4500 insta::assert_snapshot!(env.render_ok(r#"coalesce()"#), @"");
4501 insta::assert_snapshot!(env.render_ok(r#"coalesce("")"#), @"");
4502 insta::assert_snapshot!(env.render_ok(r#"coalesce("", "a", "", "b")"#), @"a");
4503 insta::assert_snapshot!(
4504 env.render_ok(r#"coalesce(empty_string, "", non_empty_string)"#), @"a");
4505
4506 insta::assert_snapshot!(env.render_ok(r#"coalesce(false, true)"#), @"false");
4508
4509 insta::assert_snapshot!(env.render_ok(r#"coalesce(bad_string, "a")"#), @"<Error: Bad>");
4511 insta::assert_snapshot!(env.render_ok(r#"coalesce("a", bad_string)"#), @"a");
4513
4514 insta::assert_snapshot!(env.parse_err(r#"coalesce("a", value2="b")"#), @r#"
4516 --> 1:15
4517 |
4518 1 | coalesce("a", value2="b")
4519 | ^--------^
4520 |
4521 = Function `coalesce`: Unexpected keyword arguments
4522 "#);
4523 }
4524
4525 #[test]
4526 fn test_concat_function() {
4527 let mut env = TestTemplateEnv::new();
4528 env.add_keyword("empty", || literal(true));
4529 env.add_keyword("hidden", || literal(false));
4530 env.add_color("empty", crossterm::style::Color::DarkGreen);
4531 env.add_color("error", crossterm::style::Color::DarkRed);
4532 env.add_color("warning", crossterm::style::Color::DarkYellow);
4533
4534 insta::assert_snapshot!(env.render_ok(r#"concat()"#), @"");
4535 insta::assert_snapshot!(
4536 env.render_ok(r#"concat(hidden, empty)"#),
4537 @"false[38;5;2mtrue[39m");
4538 insta::assert_snapshot!(
4539 env.render_ok(r#"concat(label("error", ""), label("warning", "a"), "b")"#),
4540 @"[38;5;3ma[39mb");
4541
4542 insta::assert_snapshot!(env.parse_err(r#"concat("a", value2="b")"#), @r#"
4544 --> 1:13
4545 |
4546 1 | concat("a", value2="b")
4547 | ^--------^
4548 |
4549 = Function `concat`: Unexpected keyword arguments
4550 "#);
4551 }
4552
4553 #[test]
4554 fn test_join_function() {
4555 let mut env = TestTemplateEnv::new();
4556 env.add_keyword("description", || literal("".to_owned()));
4557 env.add_keyword("empty", || literal(true));
4558 env.add_keyword("hidden", || literal(false));
4559 env.add_color("empty", crossterm::style::Color::DarkGreen);
4560 env.add_color("error", crossterm::style::Color::DarkRed);
4561 env.add_color("warning", crossterm::style::Color::DarkYellow);
4562
4563 insta::assert_snapshot!(env.render_ok(r#"join(",")"#), @"");
4565 insta::assert_snapshot!(env.render_ok(r#"join(",", "")"#), @"");
4566 insta::assert_snapshot!(env.render_ok(r#"join(",", "a")"#), @"a");
4567 insta::assert_snapshot!(env.render_ok(r#"join(",", "a", "b")"#), @"a,b");
4568 insta::assert_snapshot!(env.render_ok(r#"join(",", "a", "", "b")"#), @"a,,b");
4569 insta::assert_snapshot!(env.render_ok(r#"join(",", "a", "b", "")"#), @"a,b,");
4570 insta::assert_snapshot!(env.render_ok(r#"join(",", "", "a", "b")"#), @",a,b");
4571 insta::assert_snapshot!(
4572 env.render_ok(r#"join("--", 1, "", true, "test", "")"#),
4573 @"1----true--test--");
4574
4575 insta::assert_snapshot!(env.parse_err(r#"join()"#), @"
4577 --> 1:6
4578 |
4579 1 | join()
4580 | ^
4581 |
4582 = Function `join`: Expected at least 1 arguments
4583 ");
4584
4585 insta::assert_snapshot!(
4587 env.render_ok(r#"join(",", label("error", ""), label("warning", "a"), "b")"#),
4588 @",[38;5;3ma[39m,b");
4589 insta::assert_snapshot!(
4590 env.render_ok(
4591 r#"join(label("empty", "<>"), label("error", "a"), label("warning", ""), "b")"#),
4592 @"[38;5;1ma[38;5;2m<><>[39mb");
4593
4594 insta::assert_snapshot!(env.render_ok(r#"join(",", "a", ("" ++ ""))"#), @"a,");
4596 insta::assert_snapshot!(env.render_ok(r#"join(",", "a", ("" ++ "b"))"#), @"a,b");
4597
4598 insta::assert_snapshot!(
4600 env.render_ok(r#"join(",", "a", join("|", "", ""))"#), @"a,|");
4601 insta::assert_snapshot!(
4602 env.render_ok(r#"join(",", "a", join("|", "b", ""))"#), @"a,b|");
4603 insta::assert_snapshot!(
4604 env.render_ok(r#"join(",", "a", join("|", "b", "c"))"#), @"a,b|c");
4605
4606 insta::assert_snapshot!(
4608 env.render_ok(r#"join(",", hidden, description, empty)"#),
4609 @"false,,[38;5;2mtrue[39m");
4610 insta::assert_snapshot!(
4611 env.render_ok(r#"join(hidden, "X", "Y", "Z")"#),
4612 @"XfalseYfalseZ");
4613 insta::assert_snapshot!(
4614 env.render_ok(r#"join(hidden, empty)"#),
4615 @"[38;5;2mtrue[39m");
4616
4617 insta::assert_snapshot!(env.parse_err(r#"join(",", "a", arg="b")"#), @r#"
4619 --> 1:16
4620 |
4621 1 | join(",", "a", arg="b")
4622 | ^-----^
4623 |
4624 = Function `join`: Unexpected keyword arguments
4625 "#);
4626
4627 env.add_keyword("str_list", || {
4629 literal(vec!["foo".to_owned(), "bar".to_owned()])
4630 });
4631 env.add_keyword("none_int", || literal(None::<i64>));
4632 env.add_keyword("some_int", || literal(Some(67)));
4633 env.add_keyword("cfg_val", || {
4634 literal(ConfigValue::from_iter([("foo", "bar")]))
4635 });
4636 env.add_keyword("email", || literal(Email("me@example.com".to_owned())));
4637 env.add_keyword("signature", || {
4638 literal(new_signature("User", "user@example.com"))
4639 });
4640 env.add_keyword("size_hint", || literal((10, None)));
4641 env.add_keyword("timestamp", || literal(new_timestamp(0, 0)));
4642 env.add_keyword("timestamp_range", || {
4643 literal(TimestampRange {
4644 start: new_timestamp(0, 0),
4645 end: new_timestamp(0, 0),
4646 })
4647 });
4648 insta::assert_snapshot!(
4649 env.render_ok("join('|', str_list, 42, none_int, some_int)"),
4650 @"foo bar|42||67");
4651 insta::assert_snapshot!(
4652 env.render_ok("join('|', cfg_val, email, signature)"),
4653 @r#"{ foo = "bar" }|me@example.com|User <user@example.com>"#);
4654 insta::assert_snapshot!(
4655 env.render_ok("join('|', timestamp, timestamp_range, str_list.map(|x| x))"),
4656 @"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");
4657 assert_matches!(
4658 env.parse_err_kind("join('|', size_hint)"),
4659 TemplateParseErrorKind::Expression(_)
4660 );
4661 }
4662
4663 #[test]
4664 fn test_separate_function() {
4665 let mut env = TestTemplateEnv::new();
4666 env.add_keyword("description", || literal("".to_owned()));
4667 env.add_keyword("empty", || literal(true));
4668 env.add_keyword("hidden", || literal(false));
4669 env.add_color("empty", crossterm::style::Color::DarkGreen);
4670 env.add_color("error", crossterm::style::Color::DarkRed);
4671 env.add_color("warning", crossterm::style::Color::DarkYellow);
4672
4673 insta::assert_snapshot!(env.render_ok(r#"separate(" ")"#), @"");
4674 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "")"#), @"");
4675 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a")"#), @"a");
4676 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", "b")"#), @"a b");
4677 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", "", "b")"#), @"a b");
4678 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", "b", "")"#), @"a b");
4679 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "", "a", "b")"#), @"a b");
4680
4681 insta::assert_snapshot!(
4683 env.render_ok(r#"separate(" ", label("error", ""), label("warning", "a"), "b")"#),
4684 @"[38;5;3ma[39m b");
4685
4686 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", ("" ++ ""))"#), @"a");
4688 insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", ("" ++ "b"))"#), @"a b");
4689
4690 insta::assert_snapshot!(
4692 env.render_ok(r#"separate(" ", "a", separate("|", "", ""))"#), @"a");
4693 insta::assert_snapshot!(
4694 env.render_ok(r#"separate(" ", "a", separate("|", "b", ""))"#), @"a b");
4695 insta::assert_snapshot!(
4696 env.render_ok(r#"separate(" ", "a", separate("|", "b", "c"))"#), @"a b|c");
4697
4698 insta::assert_snapshot!(
4700 env.render_ok(r#"separate(" ", "a", if(true, ""))"#), @"a");
4701 insta::assert_snapshot!(
4702 env.render_ok(r#"separate(" ", "a", if(true, "", "f"))"#), @"a");
4703 insta::assert_snapshot!(
4704 env.render_ok(r#"separate(" ", "a", if(false, "t"))"#), @"a");
4705 insta::assert_snapshot!(
4706 env.render_ok(r#"separate(" ", "a", if(false, "t", ""))"#), @"a");
4707 insta::assert_snapshot!(
4708 env.render_ok(r#"separate(" ", "a", if(true, "t", "f"))"#), @"a t");
4709
4710 insta::assert_snapshot!(
4712 env.render_ok(r#"separate(" ", hidden, description, empty)"#),
4713 @"false [38;5;2mtrue[39m");
4714
4715 insta::assert_snapshot!(
4717 env.render_ok(r#"separate(hidden, "X", "Y", "Z")"#),
4718 @"XfalseYfalseZ");
4719
4720 insta::assert_snapshot!(env.parse_err(r#"separate(" ", "a", value2="b")"#), @r#"
4722 --> 1:20
4723 |
4724 1 | separate(" ", "a", value2="b")
4725 | ^--------^
4726 |
4727 = Function `separate`: Unexpected keyword arguments
4728 "#);
4729 }
4730
4731 #[test]
4732 fn test_surround_function() {
4733 let mut env = TestTemplateEnv::new();
4734 env.add_keyword("lt", || literal("<".to_owned()));
4735 env.add_keyword("gt", || literal(">".to_owned()));
4736 env.add_keyword("content", || literal("content".to_owned()));
4737 env.add_keyword("empty_content", || literal("".to_owned()));
4738 env.add_color("error", crossterm::style::Color::DarkRed);
4739 env.add_color("paren", crossterm::style::Color::Cyan);
4740
4741 insta::assert_snapshot!(env.render_ok(r#"surround("{", "}", "")"#), @"");
4742 insta::assert_snapshot!(env.render_ok(r#"surround("{", "}", "a")"#), @"{a}");
4743
4744 insta::assert_snapshot!(
4746 env.render_ok(
4747 r#"surround(label("paren", "("), label("paren", ")"), label("error", "a"))"#),
4748 @"[38;5;14m([38;5;1ma[38;5;14m)[39m");
4749
4750 insta::assert_snapshot!(
4752 env.render_ok(r#"surround(lt, gt, content)"#),
4753 @"<content>");
4754 insta::assert_snapshot!(
4755 env.render_ok(r#"surround(lt, gt, empty_content)"#),
4756 @"");
4757
4758 insta::assert_snapshot!(
4760 env.render_ok(r#"surround(lt, gt, if(empty_content, "", "empty"))"#),
4761 @"<empty>");
4762 insta::assert_snapshot!(
4763 env.render_ok(r#"surround(lt, gt, if(empty_content, "not empty", ""))"#),
4764 @"");
4765 }
4766
4767 #[test]
4768 fn test_config_function() {
4769 use jj_lib::config::ConfigLayer;
4770 use jj_lib::config::ConfigSource;
4771
4772 let mut config = StackedConfig::with_defaults();
4773 config
4774 .add_layer(ConfigLayer::parse(ConfigSource::User, "user.name = 'Test User'").unwrap());
4775 config.add_layer(
4776 ConfigLayer::parse(ConfigSource::User, "user.email = 'test@example.com'").unwrap(),
4777 );
4778
4779 let env = TestTemplateEnv::with_config(config);
4780
4781 insta::assert_snapshot!(env.render_ok(r#"config("user.name")"#), @"'Test User'");
4783 insta::assert_snapshot!(env.render_ok(r#"config("user.email")"#), @"'test@example.com'");
4784 insta::assert_snapshot!(env.render_ok(r#"config("user")"#), @"{ email = 'test@example.com', name = 'Test User' }");
4785
4786 insta::assert_snapshot!(env.render_ok(r#"config("non.existent")"#), @"");
4788
4789 insta::assert_snapshot!(env.render_ok(r#"if(config("user.name"), "yes", "no")"#), @"yes");
4791 insta::assert_snapshot!(env.render_ok(r#"if(config("non.existent"), "yes", "no")"#), @"no");
4792
4793 insta::assert_snapshot!(env.parse_err("config('user|name')"), @"
4795 --> 1:8
4796 |
4797 1 | config('user|name')
4798 | ^---------^
4799 |
4800 = Failed to parse config name
4801 TOML parse error at line 1, column 5
4802 |
4803 1 | user|name
4804 | ^
4805 invalid unquoted key, expected letters, numbers, `-`, `_`
4806 ");
4807 }
4808
4809 #[test]
4810 fn test_any_type() {
4811 let mut env = TestTemplateEnv::new();
4812 env.add_keyword("size_hint", || literal((5, None)));
4813 env.add_keyword("size_hint_2", || literal((10, None)));
4814 env.add_keyword("words", || {
4815 literal(vec!["foo".to_owned(), "bar".to_owned()])
4816 });
4817 env.add_color("red", crossterm::style::Color::Red);
4818
4819 insta::assert_snapshot!(env.render_ok(r#"if(true, label("red", "a"), "b")"#), @"[38;5;9ma[39m");
4821 insta::assert_snapshot!(env.render_ok(r#"if(false, label("red", "a"), "b")"#), @"b");
4822 insta::assert_snapshot!(env.render_ok(r#"json(if(true, size_hint, size_hint_2))"#), @"[5,null]");
4823 insta::assert_snapshot!(env.render_ok(r#"json(if(false, size_hint, size_hint_2))"#), @"[10,null]");
4824
4825 insta::assert_snapshot!(env.parse_err(r#"if(true, label("red", "a"), size_hint)"#), @r#"
4828 --> 1:1
4829 |
4830 1 | if(true, label("red", "a"), size_hint)
4831 | ^------------------------------------^
4832 |
4833 = Expected expression of type `Template`, but actual type is `Any`
4834 "#);
4835 insta::assert_snapshot!(env.parse_err(r#"json(if(true, size_hint, label("red", "a")))"#), @r#"
4836 --> 1:6
4837 |
4838 1 | json(if(true, size_hint, label("red", "a")))
4839 | ^------------------------------------^
4840 |
4841 = Expected expression of type `Serialize`, but actual type is `Any`
4842 "#);
4843
4844 insta::assert_snapshot!(env.parse_err(r#"if(true,words,words).join(", ")"#), @r#"
4846 --> 1:22
4847 |
4848 1 | if(true,words,words).join(", ")
4849 | ^--^
4850 |
4851 = Method `join` doesn't exist for type `Any`
4852 "#);
4853 }
4854
4855 #[test]
4856 fn test_any_list_type() {
4857 let mut env = TestTemplateEnv::new();
4858 env.add_keyword("words", || {
4859 literal(vec!["foo".to_owned(), "bar".to_owned()])
4860 });
4861 env.add_keyword("size_hint", || literal((10, None)));
4862 env.add_color("red", crossterm::style::Color::Red);
4863
4864 insta::assert_snapshot!(env.render_ok(
4866 r#"words.map(|x| label("red", x))"#),
4867 @"[38;5;9mfoo[39m [38;5;9mbar[39m");
4868 insta::assert_snapshot!(env.render_ok(
4869 r#"words.map(|x| label("red", x)).join(",")"#),
4870 @"[38;5;9mfoo[39m,[38;5;9mbar[39m");
4871 insta::assert_snapshot!(env.render_ok(
4872 r#"json(words.map(|x| size_hint))"#),
4873 @"[[10,null],[10,null]]");
4874
4875 insta::assert_snapshot!(env.parse_err(r#"words.map(|x| size_hint)"#), @r#"
4877 --> 1:1
4878 |
4879 1 | words.map(|x| size_hint)
4880 | ^----------------------^
4881 |
4882 = Expected expression of type `Template`, but actual type is `AnyList`
4883 "#);
4884 insta::assert_snapshot!(env.parse_err(r#"words.map(|x| size_hint).join(",")"#), @r#"
4885 --> 1:26
4886 |
4887 1 | words.map(|x| size_hint).join(",")
4888 | ^--^
4889 |
4890 = Expected expression of type `Template`, but actual type is `AnyList`
4891 "#);
4892 insta::assert_snapshot!(env.parse_err(r#"json(words.map(|x| label("red", x)))"#), @r#"
4893 --> 1:6
4894 |
4895 1 | json(words.map(|x| label("red", x)))
4896 | ^----------------------------^
4897 |
4898 = Expected expression of type `Serialize`, but actual type is `AnyList`
4899 "#);
4900 }
4901}