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