Files
jj-vcs-jj/cli/src/template_builder.rs
Niko Savola 60b21fda8d dsl: add description support to AliasesMap
Allows AliasesMap to store and retrieve optional descriptions for each
alias. This is a prerequisite for surfacing these descriptions in
shell completions.
2026-05-11 11:43:22 +00:00

5749 lines
241 KiB
Rust
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright 2020-2023 The Jujutsu Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::cmp::Ordering;
use std::collections::HashMap;
use std::io;
use std::iter;
use bstr::BString;
use bstr::ByteSlice as _;
use itertools::Itertools as _;
use jj_lib::backend::Signature;
use jj_lib::backend::Timestamp;
use jj_lib::config::ConfigGetResultExt as _;
use jj_lib::config::ConfigNamePathBuf;
use jj_lib::config::ConfigValue;
use jj_lib::content_hash::blake2b_hash;
use jj_lib::hex_util;
use jj_lib::op_store::TimestampRange;
use jj_lib::settings::UserSettings;
use jj_lib::str_util::StringPattern;
use jj_lib::time_util::DatePattern;
use serde::Deserialize;
use serde::de::IntoDeserializer as _;
use crate::config;
use crate::formatter::FormatRecorder;
use crate::formatter::Formatter;
use crate::template_parser;
use crate::template_parser::BinaryOp;
use crate::template_parser::ExpressionKind;
use crate::template_parser::ExpressionNode;
use crate::template_parser::FunctionCallNode;
use crate::template_parser::LambdaNode;
use crate::template_parser::TemplateAliasesMap;
use crate::template_parser::TemplateDiagnostics;
use crate::template_parser::TemplateParseError;
use crate::template_parser::TemplateParseErrorKind;
use crate::template_parser::TemplateParseResult;
use crate::template_parser::UnaryOp;
use crate::templater::AnyTemplateProperty;
use crate::templater::BoxedAnyProperty;
use crate::templater::BoxedSerializeProperty;
use crate::templater::BoxedTemplateProperty;
use crate::templater::CoalesceTemplate;
use crate::templater::ConcatTemplate;
use crate::templater::ConditionalProperty;
use crate::templater::Email;
use crate::templater::HyperlinkTemplate;
use crate::templater::JoinTemplate;
use crate::templater::LabelTemplate;
use crate::templater::ListMapProperty;
use crate::templater::ListPropertyTemplate;
use crate::templater::Literal;
use crate::templater::PlainTextFormattedProperty;
use crate::templater::PropertyPlaceholder;
use crate::templater::RawEscapeSequenceTemplate;
use crate::templater::ReformatTemplate;
use crate::templater::RegexCaptures;
use crate::templater::SeparateTemplate;
use crate::templater::SizeHint;
use crate::templater::Template;
use crate::templater::TemplateFormatter;
use crate::templater::TemplateProperty;
use crate::templater::TemplatePropertyError;
use crate::templater::TemplatePropertyExt as _;
use crate::templater::TemplateRenderer;
use crate::templater::WrapTemplateProperty;
use crate::text_util;
use crate::text_util::write_replaced;
use crate::time_util;
/// Callbacks to build usage-context-specific evaluation objects from AST nodes.
///
/// This is used to implement different meanings of `self` or different
/// globally available functions in the template language depending on the
/// context in which it is invoked.
pub trait TemplateLanguage<'a> {
type Property: CoreTemplatePropertyVar<'a> + 'a;
fn settings(&self) -> &UserSettings;
/// Translates the given global `function` call to a property.
///
/// This should be delegated to
/// `CoreTemplateBuildFnTable::build_function()`.
fn build_function(
&self,
diagnostics: &mut TemplateDiagnostics,
build_ctx: &BuildContext<Self::Property>,
function: &FunctionCallNode,
) -> TemplateParseResult<Self::Property>;
/// Creates a method call thunk for the given `function` of the given
/// `property`.
fn build_method(
&self,
diagnostics: &mut TemplateDiagnostics,
build_ctx: &BuildContext<Self::Property>,
property: Self::Property,
function: &FunctionCallNode,
) -> TemplateParseResult<Self::Property>;
}
/// Implements [`WrapTemplateProperty<'a, O>`] for property types.
///
/// - `impl_property_wrappers!(Kind { Foo(Foo), FooList(Vec<Foo>), .. });` to
/// implement conversion from types `Foo`, `Vec<Foo>`, ...
/// - `impl_property_wrappers!(<'a> Kind<'a> { .. });` for types with lifetime.
/// - `impl_property_wrappers!(Kind => Core { .. });` to forward conversion to
/// `Kind::Core(_)`.
macro_rules! impl_property_wrappers {
($kind:path $(=> $var:ident)? { $($body:tt)* }) => {
$crate::template_builder::_impl_property_wrappers_many!(
[], 'static, $kind $(=> $var)?, { $($body)* });
};
// capture the first lifetime as the lifetime of template objects.
(<$a:lifetime $(, $p:lifetime)* $(, $q:ident)*>
$kind:path $(=> $var:ident)? { $($body:tt)* }) => {
$crate::template_builder::_impl_property_wrappers_many!(
[$a, $($p,)* $($q,)*], $a, $kind $(=> $var)?, { $($body)* });
};
}
macro_rules! _impl_property_wrappers_many {
// lifetime/type parameters are packed in order to disable zipping.
// https://github.com/rust-lang/rust/issues/96184#issuecomment-1294999418
($ps:tt, $a:lifetime, $kind:path, { $( $var:ident($ty:ty), )* }) => {
$(
$crate::template_builder::_impl_property_wrappers_one!(
$ps, $a, $kind, $var, $ty, std::convert::identity);
)*
};
// variant part in body is ignored so the same body can be reused for
// implementing forwarding conversion.
($ps:tt, $a:lifetime, $kind:path => $var:ident, { $( $ignored_var:ident($ty:ty), )* }) => {
$(
$crate::template_builder::_impl_property_wrappers_one!(
$ps, $a, $kind, $var, $ty, $crate::templater::WrapTemplateProperty::wrap_property);
)*
};
}
macro_rules! _impl_property_wrappers_one {
([$($p:tt)*], $a:lifetime, $kind:path, $var:ident, $ty:ty, $inner:path) => {
impl<$($p)*> $crate::templater::WrapTemplateProperty<$a, $ty> for $kind {
fn wrap_property(property: $crate::templater::BoxedTemplateProperty<$a, $ty>) -> Self {
Self::$var($inner(property))
}
}
};
}
pub(crate) use _impl_property_wrappers_many;
pub(crate) use _impl_property_wrappers_one;
pub(crate) use impl_property_wrappers;
/// Wrapper for the core template property types.
pub trait CoreTemplatePropertyVar<'a>
where
Self: WrapTemplateProperty<'a, BString>,
Self: WrapTemplateProperty<'a, Vec<BString>>,
Self: WrapTemplateProperty<'a, String>,
Self: WrapTemplateProperty<'a, Vec<String>>,
Self: WrapTemplateProperty<'a, bool>,
Self: WrapTemplateProperty<'a, i64>,
Self: WrapTemplateProperty<'a, Option<i64>>,
Self: WrapTemplateProperty<'a, ConfigValue>,
Self: WrapTemplateProperty<'a, Option<ConfigValue>>,
Self: WrapTemplateProperty<'a, Signature>,
Self: WrapTemplateProperty<'a, Email>,
Self: WrapTemplateProperty<'a, SizeHint>,
Self: WrapTemplateProperty<'a, RegexCaptures>,
Self: WrapTemplateProperty<'a, Timestamp>,
Self: WrapTemplateProperty<'a, TimestampRange>,
{
fn wrap_template(template: Box<dyn Template + 'a>) -> Self;
fn wrap_any(property: BoxedAnyProperty<'a>) -> Self;
fn wrap_any_list(property: BoxedAnyProperty<'a>) -> Self;
/// Type name of the property output.
fn type_name(&self) -> &'static str;
/// Extracts property of `ByteString` type or newtype.
fn try_into_byte_string(self) -> Result<BoxedTemplateProperty<'a, BString>, Self>;
/// Extracts property of `String` type or newtype.
fn try_into_string(self) -> Result<BoxedTemplateProperty<'a, String>, Self>;
// TODO: rename try_into_boolean() because it isn't a pure extraction fn?
fn try_into_boolean(self) -> Result<BoxedTemplateProperty<'a, bool>, Self>;
fn try_into_integer(self) -> Result<BoxedTemplateProperty<'a, i64>, Self>;
fn try_into_timestamp(self) -> Result<BoxedTemplateProperty<'a, Timestamp>, Self>;
fn try_into_serialize(self) -> Option<BoxedSerializeProperty<'a>>;
fn try_into_template(self) -> Option<Box<dyn Template + 'a>>;
/// Transforms into a property that will evaluate to `self == other`.
fn try_into_eq(self, other: Self) -> Option<BoxedTemplateProperty<'a, bool>>;
/// Transforms into a property that will evaluate to an [`Ordering`].
fn try_into_cmp(self, other: Self) -> Option<BoxedTemplateProperty<'a, Ordering>>;
}
pub enum CoreTemplatePropertyKind<'a> {
ByteString(BoxedTemplateProperty<'a, BString>),
ByteStringList(BoxedTemplateProperty<'a, Vec<BString>>),
String(BoxedTemplateProperty<'a, String>),
StringList(BoxedTemplateProperty<'a, Vec<String>>),
Boolean(BoxedTemplateProperty<'a, bool>),
Integer(BoxedTemplateProperty<'a, i64>),
IntegerOpt(BoxedTemplateProperty<'a, Option<i64>>),
ConfigValue(BoxedTemplateProperty<'a, ConfigValue>),
ConfigValueOpt(BoxedTemplateProperty<'a, Option<ConfigValue>>),
Signature(BoxedTemplateProperty<'a, Signature>),
Email(BoxedTemplateProperty<'a, Email>),
SizeHint(BoxedTemplateProperty<'a, SizeHint>),
RegexCaptures(BoxedTemplateProperty<'a, RegexCaptures>),
Timestamp(BoxedTemplateProperty<'a, Timestamp>),
TimestampRange(BoxedTemplateProperty<'a, TimestampRange>),
// Both TemplateProperty and Template can represent a value to be evaluated
// dynamically, which suggests that `Box<dyn Template + 'a>` could be
// composed as `Box<dyn TemplateProperty<Output = Box<dyn Template ..`.
// However, there's a subtle difference: TemplateProperty is strict on
// error, whereas Template is usually lax and prints an error inline. If
// `concat(x, y)` were a property returning Template, and if `y` failed to
// evaluate, the whole expression would fail. In this example, a partial
// evaluation output is more useful. That's one reason why Template isn't
// wrapped in a TemplateProperty. Another reason is that the outermost
// caller expects a Template, not a TemplateProperty of Template output.
Template(Box<dyn Template + 'a>),
Any(BoxedAnyProperty<'a>),
AnyList(BoxedAnyProperty<'a>),
}
/// Implements `WrapTemplateProperty<type>` for core property types.
///
/// Use `impl_core_property_wrappers!(<'a> Kind<'a> => Core);` to implement
/// forwarding conversion.
macro_rules! impl_core_property_wrappers {
($($head:tt)+) => {
$crate::template_builder::impl_property_wrappers!($($head)+ {
ByteString(bstr::BString),
ByteStringList(Vec<bstr::BString>),
String(String),
StringList(Vec<String>),
Boolean(bool),
Integer(i64),
IntegerOpt(Option<i64>),
ConfigValue(jj_lib::config::ConfigValue),
ConfigValueOpt(Option<jj_lib::config::ConfigValue>),
Signature(jj_lib::backend::Signature),
Email($crate::templater::Email),
SizeHint($crate::templater::SizeHint),
RegexCaptures($crate::templater::RegexCaptures),
Timestamp(jj_lib::backend::Timestamp),
TimestampRange(jj_lib::op_store::TimestampRange),
});
};
}
pub(crate) use impl_core_property_wrappers;
impl_core_property_wrappers!(<'a> CoreTemplatePropertyKind<'a>);
impl<'a> CoreTemplatePropertyVar<'a> for CoreTemplatePropertyKind<'a> {
fn wrap_template(template: Box<dyn Template + 'a>) -> Self {
Self::Template(template)
}
fn wrap_any(property: BoxedAnyProperty<'a>) -> Self {
Self::Any(property)
}
fn wrap_any_list(property: BoxedAnyProperty<'a>) -> Self {
Self::AnyList(property)
}
fn type_name(&self) -> &'static str {
match self {
Self::ByteString(_) => "ByteString",
Self::ByteStringList(_) => "List<ByteString>",
Self::String(_) => "String",
Self::StringList(_) => "List<String>",
Self::Boolean(_) => "Boolean",
Self::Integer(_) => "Integer",
Self::IntegerOpt(_) => "Option<Integer>",
Self::ConfigValue(_) => "ConfigValue",
Self::ConfigValueOpt(_) => "Option<ConfigValue>",
Self::Signature(_) => "Signature",
Self::Email(_) => "Email",
Self::SizeHint(_) => "SizeHint",
Self::RegexCaptures(_) => "RegexCaptures",
Self::Timestamp(_) => "Timestamp",
Self::TimestampRange(_) => "TimestampRange",
Self::Template(_) => "Template",
Self::Any(_) => "Any",
Self::AnyList(_) => "AnyList",
}
}
fn try_into_byte_string(self) -> Result<BoxedTemplateProperty<'a, BString>, Self> {
match self {
Self::ByteString(property) => Ok(property),
_ => Err(self),
}
}
fn try_into_string(self) -> Result<BoxedTemplateProperty<'a, String>, Self> {
match self {
Self::String(property) => Ok(property),
_ => Err(self),
}
}
fn try_into_boolean(self) -> Result<BoxedTemplateProperty<'a, bool>, Self> {
match self {
Self::ByteString(property) => Ok(property.map(|s| !s.is_empty()).into_dyn()),
Self::ByteStringList(property) => Ok(property.map(|l| !l.is_empty()).into_dyn()),
Self::String(property) => Ok(property.map(|s| !s.is_empty()).into_dyn()),
Self::StringList(property) => Ok(property.map(|l| !l.is_empty()).into_dyn()),
Self::Boolean(property) => Ok(property),
Self::Integer(_) => Err(self),
Self::IntegerOpt(property) => Ok(property.map(|opt| opt.is_some()).into_dyn()),
Self::ConfigValue(_) => Err(self),
Self::ConfigValueOpt(property) => Ok(property.map(|opt| opt.is_some()).into_dyn()),
Self::Signature(_) => Err(self),
Self::Email(property) => Ok(property.map(|e| !e.0.is_empty()).into_dyn()),
Self::SizeHint(_) => Err(self),
Self::RegexCaptures(_) => Err(self),
Self::Timestamp(_) => Err(self),
Self::TimestampRange(_) => Err(self),
// Template and AnyList types could also be evaluated to boolean,
// but it's less likely to apply label() or .map() and use the
// result as conditional.
Self::Template(_) => Err(self),
Self::Any(_) => Err(self),
Self::AnyList(_) => Err(self),
}
}
fn try_into_integer(self) -> Result<BoxedTemplateProperty<'a, i64>, Self> {
match self {
Self::Integer(property) => Ok(property),
Self::IntegerOpt(property) => Ok(property.try_unwrap("Integer").into_dyn()),
_ => Err(self),
}
}
fn try_into_timestamp(self) -> Result<BoxedTemplateProperty<'a, Timestamp>, Self> {
match self {
Self::Timestamp(property) => Ok(property),
_ => Err(self),
}
}
fn try_into_serialize(self) -> Option<BoxedSerializeProperty<'a>> {
match self {
Self::ByteString(property) => Some(property.into_serialize()),
Self::ByteStringList(property) => Some(property.into_serialize()),
Self::String(property) => Some(property.into_serialize()),
Self::StringList(property) => Some(property.into_serialize()),
Self::Boolean(property) => Some(property.into_serialize()),
Self::Integer(property) => Some(property.into_serialize()),
Self::IntegerOpt(property) => Some(property.into_serialize()),
Self::ConfigValue(property) => {
Some(property.map(config::to_serializable_value).into_serialize())
}
Self::ConfigValueOpt(property) => Some(
property
.map(|opt| opt.map(config::to_serializable_value))
.into_serialize(),
),
Self::Signature(property) => Some(property.into_serialize()),
Self::Email(property) => Some(property.into_serialize()),
Self::SizeHint(property) => Some(property.into_serialize()),
Self::RegexCaptures(_) => None,
Self::Timestamp(property) => Some(property.into_serialize()),
Self::TimestampRange(property) => Some(property.into_serialize()),
Self::Template(_) => None,
Self::Any(property) => property.try_into_serialize(),
Self::AnyList(property) => property.try_into_serialize(),
}
}
fn try_into_template(self) -> Option<Box<dyn Template + 'a>> {
match self {
Self::ByteString(property) => Some(property.into_template()),
Self::ByteStringList(property) => Some(property.into_template()),
Self::String(property) => Some(property.into_template()),
Self::StringList(property) => Some(property.into_template()),
Self::Boolean(property) => Some(property.into_template()),
Self::Integer(property) => Some(property.into_template()),
Self::IntegerOpt(property) => Some(property.into_template()),
Self::ConfigValue(property) => Some(property.into_template()),
Self::ConfigValueOpt(property) => Some(property.into_template()),
Self::Signature(property) => Some(property.into_template()),
Self::Email(property) => Some(property.into_template()),
Self::SizeHint(_) => None,
Self::RegexCaptures(_) => None,
Self::Timestamp(property) => Some(property.into_template()),
Self::TimestampRange(property) => Some(property.into_template()),
Self::Template(template) => Some(template),
Self::Any(property) => property.try_into_template(),
Self::AnyList(property) => property.try_into_template(),
}
}
fn try_into_eq(self, other: Self) -> Option<BoxedTemplateProperty<'a, bool>> {
match (self, other) {
(Self::ByteString(lhs), Self::ByteString(rhs)) => {
Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
}
(Self::ByteString(lhs), Self::String(rhs)) => {
Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
}
(Self::String(lhs), Self::ByteString(rhs)) => {
Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
}
(Self::String(lhs), Self::String(rhs)) => {
Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
}
(Self::String(lhs), Self::Email(rhs)) => {
Some((lhs, rhs).map(|(l, r)| l == r.0).into_dyn())
}
(Self::Boolean(lhs), Self::Boolean(rhs)) => {
Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
}
(Self::Integer(lhs), Self::Integer(rhs)) => {
Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
}
(Self::Integer(lhs), Self::IntegerOpt(rhs)) => {
Some((lhs, rhs).map(|(l, r)| Some(l) == r).into_dyn())
}
(Self::IntegerOpt(lhs), Self::Integer(rhs)) => {
Some((lhs, rhs).map(|(l, r)| l == Some(r)).into_dyn())
}
(Self::IntegerOpt(lhs), Self::IntegerOpt(rhs)) => {
Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
}
(Self::Email(lhs), Self::Email(rhs)) => {
Some((lhs, rhs).map(|(l, r)| l == r).into_dyn())
}
(Self::Email(lhs), Self::String(rhs)) => {
Some((lhs, rhs).map(|(l, r)| l.0 == r).into_dyn())
}
(Self::ByteString(_), _) => None,
(Self::ByteStringList(_), _) => None,
(Self::String(_), _) => None,
(Self::StringList(_), _) => None,
(Self::Boolean(_), _) => None,
(Self::Integer(_), _) => None,
(Self::IntegerOpt(_), _) => None,
(Self::ConfigValue(_), _) => None,
(Self::ConfigValueOpt(_), _) => None,
(Self::Signature(_), _) => None,
(Self::Email(_), _) => None,
(Self::SizeHint(_), _) => None,
(Self::RegexCaptures(_), _) => None,
(Self::Timestamp(_), _) => None,
(Self::TimestampRange(_), _) => None,
(Self::Template(_), _) => None,
(Self::Any(_), _) => None,
(Self::AnyList(_), _) => None,
}
}
fn try_into_cmp(self, other: Self) -> Option<BoxedTemplateProperty<'a, Ordering>> {
match (self, other) {
(Self::Integer(lhs), Self::Integer(rhs)) => {
Some((lhs, rhs).map(|(l, r)| l.cmp(&r)).into_dyn())
}
(Self::Integer(lhs), Self::IntegerOpt(rhs)) => {
Some((lhs, rhs).map(|(l, r)| Some(l).cmp(&r)).into_dyn())
}
(Self::IntegerOpt(lhs), Self::Integer(rhs)) => {
Some((lhs, rhs).map(|(l, r)| l.cmp(&Some(r))).into_dyn())
}
(Self::IntegerOpt(lhs), Self::IntegerOpt(rhs)) => {
Some((lhs, rhs).map(|(l, r)| l.cmp(&r)).into_dyn())
}
(Self::ByteString(_), _) => None,
(Self::ByteStringList(_), _) => None,
(Self::String(_), _) => None,
(Self::StringList(_), _) => None,
(Self::Boolean(_), _) => None,
(Self::Integer(_), _) => None,
(Self::IntegerOpt(_), _) => None,
(Self::ConfigValue(_), _) => None,
(Self::ConfigValueOpt(_), _) => None,
(Self::Signature(_), _) => None,
(Self::Email(_), _) => None,
(Self::SizeHint(_), _) => None,
(Self::RegexCaptures(_), _) => None,
(Self::Timestamp(_), _) => None,
(Self::TimestampRange(_), _) => None,
(Self::Template(_), _) => None,
(Self::Any(_), _) => None,
(Self::AnyList(_), _) => None,
}
}
}
/// Function that translates global function call node.
// The lifetime parameter 'a could be replaced with for<'a> to keep the method
// table away from a certain lifetime. That's technically more correct, but I
// couldn't find an easy way to expand that to the core template methods, which
// are defined for L: TemplateLanguage<'a>. That's why the build fn table is
// bound to a named lifetime, and therefore can't be cached statically.
pub type TemplateBuildFunctionFn<'a, L, P> =
fn(&L, &mut TemplateDiagnostics, &BuildContext<P>, &FunctionCallNode) -> TemplateParseResult<P>;
type BuildMethodFn<'a, L, T, P> = fn(
&L,
&mut TemplateDiagnostics,
&BuildContext<P>,
T,
&FunctionCallNode,
) -> TemplateParseResult<P>;
/// Function that translates method call node of self type `T`.
pub type TemplateBuildMethodFn<'a, L, T, P> = BuildMethodFn<'a, L, BoxedTemplateProperty<'a, T>, P>;
/// Function that translates method call node of `Template`.
pub type BuildTemplateMethodFn<'a, L, P> = BuildMethodFn<'a, L, Box<dyn Template + 'a>, P>;
/// Function that translates method call node of `Any*`.
pub type BuildAnyMethodFn<'a, L, P> = BuildMethodFn<'a, L, BoxedAnyProperty<'a>, P>;
/// Table of functions that translate global function call node.
pub type TemplateBuildFunctionFnMap<'a, L, P = <L as TemplateLanguage<'a>>::Property> =
HashMap<&'static str, TemplateBuildFunctionFn<'a, L, P>>;
/// Table of functions that translate method call node of self type `T`.
pub type TemplateBuildMethodFnMap<'a, L, T, P = <L as TemplateLanguage<'a>>::Property> =
HashMap<&'static str, TemplateBuildMethodFn<'a, L, T, P>>;
/// Table of functions that translate method call node of `Template`.
pub type BuildTemplateMethodFnMap<'a, L, P = <L as TemplateLanguage<'a>>::Property> =
HashMap<&'static str, BuildTemplateMethodFn<'a, L, P>>;
/// Table of functions that translate method call node of `Any*`.
pub type BuildAnyMethodFnMap<'a, L, P = <L as TemplateLanguage<'a>>::Property> =
HashMap<&'static str, BuildAnyMethodFn<'a, L, P>>;
/// Symbol table of functions and methods available in the core template.
pub struct CoreTemplateBuildFnTable<'a, L: ?Sized, P = <L as TemplateLanguage<'a>>::Property> {
pub functions: TemplateBuildFunctionFnMap<'a, L, P>,
pub byte_string_methods: TemplateBuildMethodFnMap<'a, L, BString, P>,
pub byte_string_list_methods: TemplateBuildMethodFnMap<'a, L, Vec<BString>, P>,
pub string_methods: TemplateBuildMethodFnMap<'a, L, String, P>,
pub string_list_methods: TemplateBuildMethodFnMap<'a, L, Vec<String>, P>,
pub boolean_methods: TemplateBuildMethodFnMap<'a, L, bool, P>,
pub integer_methods: TemplateBuildMethodFnMap<'a, L, i64, P>,
pub config_value_methods: TemplateBuildMethodFnMap<'a, L, ConfigValue, P>,
pub email_methods: TemplateBuildMethodFnMap<'a, L, Email, P>,
pub signature_methods: TemplateBuildMethodFnMap<'a, L, Signature, P>,
pub size_hint_methods: TemplateBuildMethodFnMap<'a, L, SizeHint, P>,
pub regex_captures_methods: TemplateBuildMethodFnMap<'a, L, RegexCaptures, P>,
pub timestamp_methods: TemplateBuildMethodFnMap<'a, L, Timestamp, P>,
pub timestamp_range_methods: TemplateBuildMethodFnMap<'a, L, TimestampRange, P>,
pub template_methods: BuildTemplateMethodFnMap<'a, L, P>,
pub any_methods: BuildAnyMethodFnMap<'a, L, P>,
pub any_list_methods: BuildAnyMethodFnMap<'a, L, P>,
}
pub fn merge_fn_map<'s, F>(base: &mut HashMap<&'s str, F>, extension: HashMap<&'s str, F>) {
for (name, function) in extension {
if base.insert(name, function).is_some() {
panic!("Conflicting template definitions for '{name}' function");
}
}
}
impl<L: ?Sized, P> CoreTemplateBuildFnTable<'_, L, P> {
pub fn empty() -> Self {
Self {
functions: HashMap::new(),
byte_string_methods: HashMap::new(),
byte_string_list_methods: HashMap::new(),
string_methods: HashMap::new(),
string_list_methods: HashMap::new(),
boolean_methods: HashMap::new(),
integer_methods: HashMap::new(),
config_value_methods: HashMap::new(),
signature_methods: HashMap::new(),
email_methods: HashMap::new(),
size_hint_methods: HashMap::new(),
regex_captures_methods: HashMap::new(),
timestamp_methods: HashMap::new(),
timestamp_range_methods: HashMap::new(),
template_methods: HashMap::new(),
any_methods: HashMap::new(),
any_list_methods: HashMap::new(),
}
}
pub fn merge(&mut self, other: Self) {
let Self {
functions,
byte_string_methods,
byte_string_list_methods,
string_methods,
string_list_methods,
boolean_methods,
integer_methods,
config_value_methods,
signature_methods,
email_methods,
size_hint_methods,
regex_captures_methods,
timestamp_methods,
timestamp_range_methods,
template_methods,
any_methods,
any_list_methods,
} = other;
merge_fn_map(&mut self.functions, functions);
merge_fn_map(&mut self.byte_string_methods, byte_string_methods);
merge_fn_map(&mut self.byte_string_list_methods, byte_string_list_methods);
merge_fn_map(&mut self.string_methods, string_methods);
merge_fn_map(&mut self.string_list_methods, string_list_methods);
merge_fn_map(&mut self.boolean_methods, boolean_methods);
merge_fn_map(&mut self.integer_methods, integer_methods);
merge_fn_map(&mut self.config_value_methods, config_value_methods);
merge_fn_map(&mut self.signature_methods, signature_methods);
merge_fn_map(&mut self.email_methods, email_methods);
merge_fn_map(&mut self.size_hint_methods, size_hint_methods);
merge_fn_map(&mut self.regex_captures_methods, regex_captures_methods);
merge_fn_map(&mut self.timestamp_methods, timestamp_methods);
merge_fn_map(&mut self.timestamp_range_methods, timestamp_range_methods);
merge_fn_map(&mut self.template_methods, template_methods);
merge_fn_map(&mut self.any_methods, any_methods);
merge_fn_map(&mut self.any_list_methods, any_list_methods);
}
}
impl<'a, L> CoreTemplateBuildFnTable<'a, L, L::Property>
where
L: TemplateLanguage<'a> + ?Sized,
{
/// Creates new symbol table containing the builtin functions and methods.
pub fn builtin() -> Self {
Self {
functions: builtin_functions(),
byte_string_methods: builtin_byte_string_methods(),
byte_string_list_methods: builtin_formattable_list_methods(),
string_methods: builtin_string_methods(),
string_list_methods: builtin_formattable_list_methods(),
boolean_methods: HashMap::new(),
integer_methods: HashMap::new(),
config_value_methods: builtin_config_value_methods(),
signature_methods: builtin_signature_methods(),
email_methods: builtin_email_methods(),
size_hint_methods: builtin_size_hint_methods(),
regex_captures_methods: builtin_regex_captures_methods(),
timestamp_methods: builtin_timestamp_methods(),
timestamp_range_methods: builtin_timestamp_range_methods(),
template_methods: HashMap::new(),
any_methods: HashMap::new(),
any_list_methods: builtin_any_list_methods(),
}
}
/// Translates the function call node `function` by using this symbol table.
pub fn build_function(
&self,
language: &L,
diagnostics: &mut TemplateDiagnostics,
build_ctx: &BuildContext<L::Property>,
function: &FunctionCallNode,
) -> TemplateParseResult<L::Property> {
let table = &self.functions;
let build = template_parser::lookup_function(table, function)?;
build(language, diagnostics, build_ctx, function)
}
/// Applies the method call node `function` to the given `property` by using
/// this symbol table.
pub fn build_method(
&self,
language: &L,
diagnostics: &mut TemplateDiagnostics,
build_ctx: &BuildContext<L::Property>,
property: CoreTemplatePropertyKind<'a>,
function: &FunctionCallNode,
) -> TemplateParseResult<L::Property> {
let type_name = property.type_name();
match property {
CoreTemplatePropertyKind::ByteString(property) => {
let table = &self.byte_string_methods;
let build = template_parser::lookup_method(type_name, table, function)?;
build(language, diagnostics, build_ctx, property, function)
}
CoreTemplatePropertyKind::ByteStringList(property) => {
let table = &self.byte_string_list_methods;
let build = template_parser::lookup_method(type_name, table, function)?;
build(language, diagnostics, build_ctx, property, function)
}
CoreTemplatePropertyKind::String(property) => {
let table = &self.string_methods;
let build = template_parser::lookup_method(type_name, table, function)?;
build(language, diagnostics, build_ctx, property, function)
}
CoreTemplatePropertyKind::StringList(property) => {
let table = &self.string_list_methods;
let build = template_parser::lookup_method(type_name, table, function)?;
build(language, diagnostics, build_ctx, property, function)
}
CoreTemplatePropertyKind::Boolean(property) => {
let table = &self.boolean_methods;
let build = template_parser::lookup_method(type_name, table, function)?;
build(language, diagnostics, build_ctx, property, function)
}
CoreTemplatePropertyKind::Integer(property) => {
let table = &self.integer_methods;
let build = template_parser::lookup_method(type_name, table, function)?;
build(language, diagnostics, build_ctx, property, function)
}
CoreTemplatePropertyKind::IntegerOpt(property) => {
let type_name = "Integer";
let table = &self.integer_methods;
let build = template_parser::lookup_method(type_name, table, function)?;
let inner_property = property.try_unwrap(type_name).into_dyn();
build(language, diagnostics, build_ctx, inner_property, function)
}
CoreTemplatePropertyKind::ConfigValue(property) => {
let table = &self.config_value_methods;
let build = template_parser::lookup_method(type_name, table, function)?;
build(language, diagnostics, build_ctx, property, function)
}
CoreTemplatePropertyKind::ConfigValueOpt(property) => {
let type_name = "ConfigValue";
let table = &self.config_value_methods;
let build = template_parser::lookup_method(type_name, table, function)?;
let inner_property = property.try_unwrap(type_name).into_dyn();
build(language, diagnostics, build_ctx, inner_property, function)
}
CoreTemplatePropertyKind::Signature(property) => {
let table = &self.signature_methods;
let build = template_parser::lookup_method(type_name, table, function)?;
build(language, diagnostics, build_ctx, property, function)
}
CoreTemplatePropertyKind::Email(property) => {
let table = &self.email_methods;
let build = template_parser::lookup_method(type_name, table, function)?;
build(language, diagnostics, build_ctx, property, function)
}
CoreTemplatePropertyKind::SizeHint(property) => {
let table = &self.size_hint_methods;
let build = template_parser::lookup_method(type_name, table, function)?;
build(language, diagnostics, build_ctx, property, function)
}
CoreTemplatePropertyKind::RegexCaptures(property) => {
let table = &self.regex_captures_methods;
let build = template_parser::lookup_method(type_name, table, function)?;
build(language, diagnostics, build_ctx, property, function)
}
CoreTemplatePropertyKind::Timestamp(property) => {
let table = &self.timestamp_methods;
let build = template_parser::lookup_method(type_name, table, function)?;
build(language, diagnostics, build_ctx, property, function)
}
CoreTemplatePropertyKind::TimestampRange(property) => {
let table = &self.timestamp_range_methods;
let build = template_parser::lookup_method(type_name, table, function)?;
build(language, diagnostics, build_ctx, property, function)
}
CoreTemplatePropertyKind::Template(template) => {
let table = &self.template_methods;
let build = template_parser::lookup_method(type_name, table, function)?;
build(language, diagnostics, build_ctx, template, function)
}
CoreTemplatePropertyKind::Any(property) => {
let table = &self.any_methods;
let build = template_parser::lookup_method(type_name, table, function)?;
build(language, diagnostics, build_ctx, property, function)
}
CoreTemplatePropertyKind::AnyList(property) => {
let table = &self.any_list_methods;
let build = template_parser::lookup_method(type_name, table, function)?;
build(language, diagnostics, build_ctx, property, function)
}
}
}
}
/// Opaque struct that represents a template value.
pub struct Expression<P> {
property: P,
labels: Vec<String>,
}
impl<P> Expression<P> {
fn unlabeled(property: P) -> Self {
let labels = vec![];
Self { property, labels }
}
fn with_label(property: P, label: impl Into<String>) -> Self {
let labels = vec![label.into()];
Self { property, labels }
}
}
impl<'a, P: CoreTemplatePropertyVar<'a>> Expression<P> {
pub fn type_name(&self) -> &'static str {
self.property.type_name()
}
pub fn try_into_boolean(self) -> Option<BoxedTemplateProperty<'a, bool>> {
self.property.try_into_boolean().ok()
}
pub fn try_into_integer(self) -> Option<BoxedTemplateProperty<'a, i64>> {
self.property.try_into_integer().ok()
}
pub fn try_into_timestamp(self) -> Option<BoxedTemplateProperty<'a, Timestamp>> {
self.property.try_into_timestamp().ok()
}
/// Transforms into a byte string property by formatting the value if
/// needed.
pub fn try_into_byte_stringify(self) -> Option<BoxedTemplateProperty<'a, BString>> {
let property = match self.property.try_into_byte_string() {
Ok(bytes_property) => return Some(bytes_property),
Err(property) => property,
};
let property = match property.try_into_string() {
Ok(string_property) => return Some(string_property.map(BString::from).into_dyn()),
Err(property) => property,
};
let template = property.try_into_template()?;
Some(PlainTextFormattedProperty::new(template).into_dyn())
}
/// Transforms into a string property by formatting the value if needed.
pub fn try_into_stringify(self) -> Option<BoxedTemplateProperty<'a, String>> {
let from_bytes =
|s: BString| Ok(String::from_utf8(s.into()).map_err(|err| err.utf8_error())?);
let property = match self.property.try_into_string() {
Ok(string_property) => return Some(string_property),
Err(property) => property,
};
let property = match property.try_into_byte_string() {
Ok(bytes_property) => return Some(bytes_property.and_then(from_bytes).into_dyn()),
Err(property) => property,
};
let template = property.try_into_template()?;
Some(
PlainTextFormattedProperty::new(template)
.and_then(from_bytes)
.into_dyn(),
)
}
pub fn try_into_serialize(self) -> Option<BoxedSerializeProperty<'a>> {
self.property.try_into_serialize()
}
pub fn try_into_template(self) -> Option<Box<dyn Template + 'a>> {
let template = self.property.try_into_template()?;
if self.labels.is_empty() {
Some(template)
} else {
Some(Box::new(LabelTemplate::new(template, Literal(self.labels))))
}
}
pub fn try_into_eq(self, other: Self) -> Option<BoxedTemplateProperty<'a, bool>> {
self.property.try_into_eq(other.property)
}
pub fn try_into_cmp(self, other: Self) -> Option<BoxedTemplateProperty<'a, Ordering>> {
self.property.try_into_cmp(other.property)
}
}
impl<'a, P: CoreTemplatePropertyVar<'a>> AnyTemplateProperty<'a> for Expression<P> {
fn try_into_serialize(self: Box<Self>) -> Option<BoxedSerializeProperty<'a>> {
(*self).try_into_serialize()
}
fn try_into_template(self: Box<Self>) -> Option<Box<dyn Template + 'a>> {
(*self).try_into_template()
}
fn try_join(self: Box<Self>, _: Box<dyn Template + 'a>) -> Option<Box<dyn Template + 'a>> {
None
}
}
/// Environment (locals and self) in a stack frame.
pub struct BuildContext<'i, P> {
/// Map of functions to create `L::Property`.
local_variables: HashMap<&'i str, &'i dyn Fn() -> P>,
/// Function to create `L::Property` representing `self`.
///
/// This could be `local_variables["self"]`, but keyword lookup shouldn't be
/// overridden by a user-defined `self` variable.
self_variable: &'i dyn Fn() -> P,
}
fn build_keyword<'a, L: TemplateLanguage<'a> + ?Sized>(
language: &L,
diagnostics: &mut TemplateDiagnostics,
build_ctx: &BuildContext<L::Property>,
name: &str,
name_span: pest::Span<'_>,
) -> TemplateParseResult<L::Property> {
// Keyword is a 0-ary method on the "self" property
let self_property = (build_ctx.self_variable)();
let function = FunctionCallNode {
name,
name_span,
args: vec![],
keyword_args: vec![],
args_span: name_span.end_pos().span(&name_span.end_pos()),
};
language
.build_method(diagnostics, build_ctx, self_property, &function)
.map_err(|err| match err.kind() {
TemplateParseErrorKind::NoSuchMethod { candidates, .. } => {
let kind = TemplateParseErrorKind::NoSuchKeyword {
name: name.to_owned(),
// TODO: filter methods by arity?
candidates: candidates.clone(),
};
TemplateParseError::with_span(kind, name_span)
}
// Since keyword is a 0-ary method, any argument errors mean there's
// no such keyword.
TemplateParseErrorKind::InvalidArguments { .. } => {
let kind = TemplateParseErrorKind::NoSuchKeyword {
name: name.to_owned(),
// TODO: might be better to phrase the error differently
candidates: vec![format!("self.{name}(..)")],
};
TemplateParseError::with_span(kind, name_span)
}
// The keyword function may fail with the other reasons.
_ => err,
})
}
fn build_unary_operation<'a, L: TemplateLanguage<'a> + ?Sized>(
language: &L,
diagnostics: &mut TemplateDiagnostics,
build_ctx: &BuildContext<L::Property>,
op: UnaryOp,
arg_node: &ExpressionNode,
) -> TemplateParseResult<L::Property> {
match op {
UnaryOp::LogicalNot => {
let arg = expect_boolean_expression(language, diagnostics, build_ctx, arg_node)?;
Ok(arg.map(|v| !v).into_dyn_wrapped())
}
UnaryOp::Negate => {
let arg = expect_integer_expression(language, diagnostics, build_ctx, arg_node)?;
let out = arg.and_then(|v| {
v.checked_neg()
.ok_or_else(|| TemplatePropertyError("Attempt to negate with overflow".into()))
});
Ok(out.into_dyn_wrapped())
}
}
}
fn build_binary_operation<'a, L: TemplateLanguage<'a> + ?Sized>(
language: &L,
diagnostics: &mut TemplateDiagnostics,
build_ctx: &BuildContext<L::Property>,
op: BinaryOp,
lhs_node: &ExpressionNode,
rhs_node: &ExpressionNode,
span: pest::Span<'_>,
) -> TemplateParseResult<L::Property> {
match op {
BinaryOp::LogicalOr => {
let lhs = expect_boolean_expression(language, diagnostics, build_ctx, lhs_node)?;
let rhs = expect_boolean_expression(language, diagnostics, build_ctx, rhs_node)?;
let out = lhs.and_then(move |l| Ok(l || rhs.extract()?));
Ok(out.into_dyn_wrapped())
}
BinaryOp::LogicalAnd => {
let lhs = expect_boolean_expression(language, diagnostics, build_ctx, lhs_node)?;
let rhs = expect_boolean_expression(language, diagnostics, build_ctx, rhs_node)?;
let out = lhs.and_then(move |l| Ok(l && rhs.extract()?));
Ok(out.into_dyn_wrapped())
}
BinaryOp::Eq | BinaryOp::Ne => {
let lhs = build_expression(language, diagnostics, build_ctx, lhs_node)?;
let rhs = build_expression(language, diagnostics, build_ctx, rhs_node)?;
let lty = lhs.type_name();
let rty = rhs.type_name();
let eq = lhs.try_into_eq(rhs).ok_or_else(|| {
let message = format!("Cannot compare expressions of type `{lty}` and `{rty}`");
TemplateParseError::expression(message, span)
})?;
let out = match op {
BinaryOp::Eq => eq.into_dyn(),
BinaryOp::Ne => eq.map(|eq| !eq).into_dyn(),
_ => unreachable!(),
};
Ok(L::Property::wrap_property(out))
}
BinaryOp::Ge | BinaryOp::Gt | BinaryOp::Le | BinaryOp::Lt => {
let lhs = build_expression(language, diagnostics, build_ctx, lhs_node)?;
let rhs = build_expression(language, diagnostics, build_ctx, rhs_node)?;
let lty = lhs.type_name();
let rty = rhs.type_name();
let cmp = lhs.try_into_cmp(rhs).ok_or_else(|| {
let message = format!("Cannot compare expressions of type `{lty}` and `{rty}`");
TemplateParseError::expression(message, span)
})?;
let out = match op {
BinaryOp::Ge => cmp.map(|ordering| ordering.is_ge()).into_dyn(),
BinaryOp::Gt => cmp.map(|ordering| ordering.is_gt()).into_dyn(),
BinaryOp::Le => cmp.map(|ordering| ordering.is_le()).into_dyn(),
BinaryOp::Lt => cmp.map(|ordering| ordering.is_lt()).into_dyn(),
_ => unreachable!(),
};
Ok(L::Property::wrap_property(out))
}
BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div | BinaryOp::Rem => {
let lhs = expect_integer_expression(language, diagnostics, build_ctx, lhs_node)?;
let rhs = expect_integer_expression(language, diagnostics, build_ctx, rhs_node)?;
let build = |op: fn(i64, i64) -> Option<i64>, msg: fn(i64) -> &'static str| {
(lhs, rhs).and_then(move |(l, r)| {
op(l, r).ok_or_else(|| TemplatePropertyError(msg(r).into()))
})
};
let out = match op {
BinaryOp::Add => build(i64::checked_add, |_| "Attempt to add with overflow"),
BinaryOp::Sub => build(i64::checked_sub, |_| "Attempt to subtract with overflow"),
BinaryOp::Mul => build(i64::checked_mul, |_| "Attempt to multiply with overflow"),
BinaryOp::Div => build(i64::checked_div, |r| {
if r == 0 {
"Attempt to divide by zero"
} else {
"Attempt to divide with overflow"
}
}),
BinaryOp::Rem => build(i64::checked_rem, |r| {
if r == 0 {
"Attempt to divide by zero"
} else {
"Attempt to divide with overflow"
}
}),
_ => unreachable!(),
};
Ok(out.into_dyn_wrapped())
}
}
}
fn builtin_byte_string_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
-> TemplateBuildMethodFnMap<'a, L, BString> {
// Not using maplit::hashmap!{} or custom declarative macro here because
// code completion inside macro is quite restricted.
let mut map = TemplateBuildMethodFnMap::<L, BString>::new();
map.insert(
"len",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.and_then(|s| Ok(i64::try_from(s.len())?));
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"contains",
|language, diagnostics, build_ctx, self_property, function| {
let [needle_node] = function.expect_exact_arguments()?;
// TODO: or .try_into_byte_string() to disable implicit type cast?
let needle_property =
expect_byte_stringify_expression(language, diagnostics, build_ctx, needle_node)?;
let out_property = (self_property, needle_property)
.map(|(haystack, needle)| haystack.contains_str(&needle));
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"match",
|_language, _diagnostics, _build_ctx, self_property, function| {
let [needle_node] = function.expect_exact_arguments()?;
let needle = template_parser::expect_string_pattern(needle_node)?;
let out_property = match_string_like(self_property, needle);
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"starts_with",
|language, diagnostics, build_ctx, self_property, function| {
let [needle_node] = function.expect_exact_arguments()?;
let needle_property =
expect_byte_stringify_expression(language, diagnostics, build_ctx, needle_node)?;
let out_property = (self_property, needle_property)
.map(|(haystack, needle)| haystack.starts_with(&needle));
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"ends_with",
|language, diagnostics, build_ctx, self_property, function| {
let [needle_node] = function.expect_exact_arguments()?;
let needle_property =
expect_byte_stringify_expression(language, diagnostics, build_ctx, needle_node)?;
let out_property = (self_property, needle_property)
.map(|(haystack, needle)| haystack.ends_with(&needle));
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"remove_prefix",
|language, diagnostics, build_ctx, self_property, function| {
let [needle_node] = function.expect_exact_arguments()?;
let needle_property =
expect_byte_stringify_expression(language, diagnostics, build_ctx, needle_node)?;
let out_property = (self_property, needle_property).map(|(haystack, needle)| {
haystack
.strip_prefix(&**needle)
.map(BString::from)
.unwrap_or(haystack)
});
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"remove_suffix",
|language, diagnostics, build_ctx, self_property, function| {
let [needle_node] = function.expect_exact_arguments()?;
let needle_property =
expect_byte_stringify_expression(language, diagnostics, build_ctx, needle_node)?;
let out_property = (self_property, needle_property).map(|(haystack, needle)| {
haystack
.strip_suffix(&**needle)
.map(BString::from)
.unwrap_or(haystack)
});
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"trim",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.map(|s| BString::from(s.trim_ascii()));
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"trim_start",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.map(|s| BString::from(s.trim_ascii_start()));
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"trim_end",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.map(|s| BString::from(s.trim_ascii_end()));
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"substr",
|language, diagnostics, build_ctx, self_property, function| {
let ([start_idx_node], [end_idx_node]) = function.expect_arguments()?;
let start_idx_property =
expect_isize_expression(language, diagnostics, build_ctx, start_idx_node)?;
let end_idx_property = end_idx_node
.map(|node| expect_isize_expression(language, diagnostics, build_ctx, node))
.transpose()?;
let out_property = (self_property, start_idx_property, end_idx_property).map(
|(s, start_idx, end_idx)| {
let start_idx = clamp_signed_bytes_index(&s, start_idx);
let end_idx = end_idx.map_or(s.len(), |idx| clamp_signed_bytes_index(&s, idx));
BString::from(s.get(start_idx..end_idx).unwrap_or_default())
},
);
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"first_line",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property =
self_property.map(|s| s.lines().next().map(BString::from).unwrap_or_default());
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"lines",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.map(|s| s.lines().map(BString::from).collect_vec());
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"split",
|language, diagnostics, build_ctx, self_property, function| {
let ([separator_node], [limit_node]) = function.expect_arguments()?;
let pattern = template_parser::expect_string_pattern(separator_node)?;
if let Some(limit_node) = limit_node {
let limit_property =
expect_usize_expression(language, diagnostics, build_ctx, limit_node)?;
let out_property = splitn_string_like(self_property, pattern, limit_property);
Ok(out_property.into_dyn_wrapped())
} else {
let out_property = split_string_like(self_property, pattern);
Ok(out_property.into_dyn_wrapped())
}
},
);
map.insert(
"replace",
|language, diagnostics, build_ctx, self_property, function| {
let ([pattern_node, replacement_node], [limit_node]) = function.expect_arguments()?;
let pattern = template_parser::expect_string_pattern(pattern_node)?;
let replacement_property = expect_byte_stringify_expression(
language,
diagnostics,
build_ctx,
replacement_node,
)?;
if let Some(limit_node) = limit_node {
let limit_property =
expect_usize_expression(language, diagnostics, build_ctx, limit_node)?;
let out_property = replacen_string_like(
self_property,
pattern,
replacement_property,
limit_property,
);
Ok(out_property.into_dyn_wrapped())
} else {
let out_property =
replace_all_string_like(self_property, pattern, replacement_property);
Ok(out_property.into_dyn_wrapped())
}
},
);
map.insert(
"upper",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.map(|s| BString::from(s.to_ascii_uppercase()));
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"lower",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.map(|s| BString::from(s.to_ascii_lowercase()));
Ok(out_property.into_dyn_wrapped())
},
);
map
}
fn builtin_string_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
-> TemplateBuildMethodFnMap<'a, L, String> {
// Not using maplit::hashmap!{} or custom declarative macro here because
// code completion inside macro is quite restricted.
let mut map = TemplateBuildMethodFnMap::<L, String>::new();
map.insert(
"len",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.and_then(|s| Ok(i64::try_from(s.len())?));
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"contains",
|language, diagnostics, build_ctx, self_property, function| {
let [needle_node] = function.expect_exact_arguments()?;
// TODO: or .try_into_string() to disable implicit type cast?
let needle_property =
expect_stringify_expression(language, diagnostics, build_ctx, needle_node)?;
let out_property = (self_property, needle_property)
.map(|(haystack, needle)| haystack.contains(&needle));
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"match",
|_language, _diagnostics, _build_ctx, self_property, function| {
let [needle_node] = function.expect_exact_arguments()?;
let needle = template_parser::expect_string_pattern(needle_node)?;
let out_property = match_string_like(self_property, needle);
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"starts_with",
|language, diagnostics, build_ctx, self_property, function| {
let [needle_node] = function.expect_exact_arguments()?;
let needle_property =
expect_stringify_expression(language, diagnostics, build_ctx, needle_node)?;
let out_property = (self_property, needle_property)
.map(|(haystack, needle)| haystack.starts_with(&needle));
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"ends_with",
|language, diagnostics, build_ctx, self_property, function| {
let [needle_node] = function.expect_exact_arguments()?;
let needle_property =
expect_stringify_expression(language, diagnostics, build_ctx, needle_node)?;
let out_property = (self_property, needle_property)
.map(|(haystack, needle)| haystack.ends_with(&needle));
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"remove_prefix",
|language, diagnostics, build_ctx, self_property, function| {
let [needle_node] = function.expect_exact_arguments()?;
let needle_property =
expect_stringify_expression(language, diagnostics, build_ctx, needle_node)?;
let out_property = (self_property, needle_property).map(|(haystack, needle)| {
haystack
.strip_prefix(&needle)
.map(ToOwned::to_owned)
.unwrap_or(haystack)
});
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"remove_suffix",
|language, diagnostics, build_ctx, self_property, function| {
let [needle_node] = function.expect_exact_arguments()?;
let needle_property =
expect_stringify_expression(language, diagnostics, build_ctx, needle_node)?;
let out_property = (self_property, needle_property).map(|(haystack, needle)| {
haystack
.strip_suffix(&needle)
.map(ToOwned::to_owned)
.unwrap_or(haystack)
});
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"trim",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.map(|s| s.trim().to_owned());
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"trim_start",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.map(|s| s.trim_start().to_owned());
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"trim_end",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.map(|s| s.trim_end().to_owned());
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"substr",
|language, diagnostics, build_ctx, self_property, function| {
let ([start_idx_node], [end_idx_node]) = function.expect_arguments()?;
let start_idx_property =
expect_isize_expression(language, diagnostics, build_ctx, start_idx_node)?;
let end_idx_property = end_idx_node
.map(|node| expect_isize_expression(language, diagnostics, build_ctx, node))
.transpose()?;
let out_property = (self_property, start_idx_property, end_idx_property).map(
|(s, start_idx, end_idx)| {
let start_idx = string_index_to_char_boundary(&s, start_idx);
let end_idx =
end_idx.map_or(s.len(), |idx| string_index_to_char_boundary(&s, idx));
s.get(start_idx..end_idx).unwrap_or_default().to_owned()
},
);
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"first_line",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property =
self_property.map(|s| s.lines().next().unwrap_or_default().to_string());
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"lines",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.map(|s| s.lines().map(|l| l.to_owned()).collect_vec());
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"split",
|language, diagnostics, build_ctx, self_property, function| {
let ([separator_node], [limit_node]) = function.expect_arguments()?;
let pattern = template_parser::expect_string_pattern(separator_node)?;
if let Some(limit_node) = limit_node {
let limit_property =
expect_usize_expression(language, diagnostics, build_ctx, limit_node)?;
let out_property = splitn_string_like(self_property, pattern, limit_property);
Ok(out_property.into_dyn_wrapped())
} else {
let out_property = split_string_like(self_property, pattern);
Ok(out_property.into_dyn_wrapped())
}
},
);
map.insert(
"replace",
|language, diagnostics, build_ctx, self_property, function| {
let ([pattern_node, replacement_node], [limit_node]) = function.expect_arguments()?;
let pattern = template_parser::expect_string_pattern(pattern_node)?;
let replacement_property =
expect_stringify_expression(language, diagnostics, build_ctx, replacement_node)?;
if let Some(limit_node) = limit_node {
let limit_property =
expect_usize_expression(language, diagnostics, build_ctx, limit_node)?;
let out_property = replacen_string_like(
self_property,
pattern,
replacement_property,
limit_property,
);
Ok(out_property.into_dyn_wrapped())
} else {
let out_property =
replace_all_string_like(self_property, pattern, replacement_property);
Ok(out_property.into_dyn_wrapped())
}
},
);
map.insert(
"upper",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.map(|s| s.to_uppercase());
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"lower",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.map(|s| s.to_lowercase());
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"escape_json",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.map(|s| serde_json::to_string(&s).unwrap());
Ok(out_property.into_dyn_wrapped())
},
);
map
}
trait StringLike: AsRef<[u8]> + Sized {
fn from_bytes(bytes: &[u8]) -> Result<Self, TemplatePropertyError>;
}
impl StringLike for BString {
fn from_bytes(bytes: &[u8]) -> Result<Self, TemplatePropertyError> {
Ok(bytes.into())
}
}
impl StringLike for String {
fn from_bytes(bytes: &[u8]) -> Result<Self, TemplatePropertyError> {
Ok(str::from_utf8(bytes)?.to_owned())
}
}
fn match_string_like<S: StringLike>(
self_property: impl TemplateProperty<Output = S>,
needle: StringPattern,
) -> impl TemplateProperty<Output = S> {
let regex = needle.to_regex();
self_property.and_then(move |haystack| {
// We don't have optional strings, so "" is the right null value.
S::from_bytes(regex.find(haystack.as_ref()).map_or(b"", |m| m.as_bytes()))
})
}
fn split_string_like<S: StringLike>(
self_property: impl TemplateProperty<Output = S>,
pattern: StringPattern,
) -> impl TemplateProperty<Output = Vec<S>> {
let regex = pattern.to_regex();
self_property
.and_then(move |haystack| regex.split(haystack.as_ref()).map(S::from_bytes).collect())
}
fn splitn_string_like<S: StringLike>(
self_property: impl TemplateProperty<Output = S>,
pattern: StringPattern,
limit_property: impl TemplateProperty<Output = usize>,
) -> impl TemplateProperty<Output = Vec<S>> {
let regex = pattern.to_regex();
(self_property, limit_property).and_then(move |(haystack, limit)| {
regex
.splitn(haystack.as_ref(), limit)
.map(S::from_bytes)
.collect()
})
}
fn replace_all_string_like<S: StringLike>(
self_property: impl TemplateProperty<Output = S>,
pattern: StringPattern,
replacement_property: impl TemplateProperty<Output = S>,
) -> impl TemplateProperty<Output = S> {
let regex = pattern.to_regex();
(self_property, replacement_property).and_then(move |(haystack, replacement)| {
S::from_bytes(&regex.replace_all(haystack.as_ref(), replacement.as_ref()))
})
}
fn replacen_string_like<S: StringLike>(
self_property: impl TemplateProperty<Output = S>,
pattern: StringPattern,
replacement_property: impl TemplateProperty<Output = S>,
limit_property: impl TemplateProperty<Output = usize>,
) -> impl TemplateProperty<Output = S> {
let regex = pattern.to_regex();
(self_property, replacement_property, limit_property).and_then(
move |(haystack, replacement, limit)| {
if limit == 0 {
// We need to special-case zero because regex.replacen(_, 0, _)
// replaces all occurrences, and we want zero to mean no
// occurrences are replaced.
Ok(haystack)
} else {
S::from_bytes(&regex.replacen(haystack.as_ref(), limit, replacement.as_ref()))
}
},
)
}
/// Clamps the given index. Negative index counts from the end.
fn clamp_signed_bytes_index(s: &[u8], i: isize) -> usize {
let magnitude = i.unsigned_abs();
if i < 0 {
s.len().saturating_sub(magnitude)
} else {
magnitude.min(s.len())
}
}
/// Clamps and aligns the given index `i` to char boundary.
///
/// Negative index counts from the end. If the index isn't at a char boundary,
/// it will be rounded towards 0 (left or right depending on the sign.)
fn string_index_to_char_boundary(s: &str, i: isize) -> usize {
// TODO: use floor/ceil_char_boundary() if get stabilized
let p = clamp_signed_bytes_index(s.as_bytes(), i);
if i < 0 {
(p..=s.len()).find(|&p| s.is_char_boundary(p)).unwrap()
} else {
(0..=p).rev().find(|&p| s.is_char_boundary(p)).unwrap()
}
}
fn builtin_config_value_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
-> TemplateBuildMethodFnMap<'a, L, ConfigValue> {
fn extract<'de, T: Deserialize<'de>>(value: ConfigValue) -> Result<T, TemplatePropertyError> {
T::deserialize(value.into_deserializer())
// map to err.message() because TomlError appends newline to it
.map_err(|err| TemplatePropertyError(err.message().into()))
}
// Not using maplit::hashmap!{} or custom declarative macro here because
// code completion inside macro is quite restricted.
let mut map = TemplateBuildMethodFnMap::<L, ConfigValue>::new();
// These methods are called "as_<type>", not "to_<type>" to clarify that
// they'll never convert types (e.g. integer to string.) Since templater
// doesn't provide binding syntax, there's no need to distinguish between
// reference and consuming access.
map.insert(
"as_boolean",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.and_then(extract::<bool>);
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"as_integer",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.and_then(extract::<i64>);
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"as_string",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.and_then(extract::<String>);
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"as_string_list",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.and_then(extract::<Vec<String>>);
Ok(out_property.into_dyn_wrapped())
},
);
// TODO: add is_<type>() -> Boolean?
// TODO: add .get(key) -> ConfigValue or Option<ConfigValue>?
map
}
fn builtin_signature_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
-> TemplateBuildMethodFnMap<'a, L, Signature> {
// Not using maplit::hashmap!{} or custom declarative macro here because
// code completion inside macro is quite restricted.
let mut map = TemplateBuildMethodFnMap::<L, Signature>::new();
map.insert(
"name",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.map(|signature| signature.name);
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"email",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.map(|signature| Email(signature.email));
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"timestamp",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.map(|signature| signature.timestamp);
Ok(out_property.into_dyn_wrapped())
},
);
map
}
fn builtin_email_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
-> TemplateBuildMethodFnMap<'a, L, Email> {
// Not using maplit::hashmap!{} or custom declarative macro here because
// code completion inside macro is quite restricted.
let mut map = TemplateBuildMethodFnMap::<L, Email>::new();
map.insert(
"local",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.map(|email| {
let (local, _) = text_util::split_email(&email.0);
local.to_owned()
});
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"domain",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.map(|email| {
let (_, domain) = text_util::split_email(&email.0);
domain.unwrap_or_default().to_owned()
});
Ok(out_property.into_dyn_wrapped())
},
);
map
}
fn builtin_size_hint_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
-> TemplateBuildMethodFnMap<'a, L, SizeHint> {
// Not using maplit::hashmap!{} or custom declarative macro here because
// code completion inside macro is quite restricted.
let mut map = TemplateBuildMethodFnMap::<L, SizeHint>::new();
map.insert(
"lower",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.and_then(|(lower, _)| Ok(i64::try_from(lower)?));
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"upper",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property =
self_property.and_then(|(_, upper)| Ok(upper.map(i64::try_from).transpose()?));
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"exact",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.and_then(|(lower, upper)| {
let exact = (Some(lower) == upper).then_some(lower);
Ok(exact.map(i64::try_from).transpose()?)
});
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"zero",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.map(|(_, upper)| upper == Some(0));
Ok(out_property.into_dyn_wrapped())
},
);
map
}
fn builtin_regex_captures_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
-> TemplateBuildMethodFnMap<'a, L, RegexCaptures> {
// Not using maplit::hashmap!{} or custom declarative macro here because
// code completion inside macro is quite restricted.
let mut map = TemplateBuildMethodFnMap::<L, RegexCaptures>::new();
map.insert(
"len",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property =
self_property.and_then(|captures| Ok(i64::try_from(captures.len())?));
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"get",
|language, diagnostics, build_ctx, self_property, function| {
let [index_node] = function.expect_exact_arguments()?;
let index = expect_usize_expression(language, diagnostics, build_ctx, index_node)?;
let out_property = (self_property, index).and_then(|(captures, index)| {
captures.get(index).ok_or_else(|| {
TemplatePropertyError(
format!("Could not get capture group with index {index}").into(),
)
})
});
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"name",
|language, diagnostics, build_ctx, self_property, function| {
let [name_node] = function.expect_exact_arguments()?;
let name = expect_stringify_expression(language, diagnostics, build_ctx, name_node)?;
let out_property = (self_property, name).and_then(move |(c, name)| {
c.name(&name).ok_or_else(|| {
TemplatePropertyError(
format!("Could not get capture group with name {name}").into(),
)
})
});
Ok(out_property.into_dyn_wrapped())
},
);
map
}
fn builtin_timestamp_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
-> TemplateBuildMethodFnMap<'a, L, Timestamp> {
// Not using maplit::hashmap!{} or custom declarative macro here because
// code completion inside macro is quite restricted.
let mut map = TemplateBuildMethodFnMap::<L, Timestamp>::new();
map.insert(
"ago",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let now = Timestamp::now();
let format = timeago::Formatter::new();
let out_property = self_property.and_then(move |timestamp| {
Ok(time_util::format_duration(&timestamp, &now, &format)?)
});
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"format",
|language, diagnostics, build_ctx, self_property, function| {
let [format_node] = function.expect_exact_arguments()?;
let format_property =
expect_stringify_expression(language, diagnostics, build_ctx, format_node)?;
if let Ok(format) = format_property.extract() {
let format = template_parser::catch_aliases(
diagnostics,
format_node,
|_diagnostics, node| {
time_util::FormattingItems::parse(&format).ok_or_else(|| {
TemplateParseError::expression("Invalid time format", node.span)
})
},
)?
.into_owned();
let out_property = self_property.and_then(move |timestamp| {
Ok(time_util::format_absolute_timestamp_with(
&timestamp, &format,
)?)
});
Ok(out_property.into_dyn_wrapped())
} else {
let out_property =
(self_property, format_property).and_then(move |(timestamp, format)| {
let format =
time_util::FormattingItems::parse(&format).ok_or_else(|| {
let message = format!("Invalid time format: {format}");
TemplatePropertyError(message.into())
})?;
Ok(time_util::format_absolute_timestamp_with(
&timestamp, &format,
)?)
});
Ok(out_property.into_dyn_wrapped())
}
},
);
map.insert(
"utc",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.map(|mut timestamp| {
timestamp.tz_offset = 0;
timestamp
});
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"local",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let tz_offset = std::env::var("JJ_TZ_OFFSET_MINS")
.ok()
.and_then(|tz_string| tz_string.parse::<i32>().ok())
.unwrap_or_else(|| chrono::Local::now().offset().local_minus_utc() / 60);
let out_property = self_property.map(move |mut timestamp| {
timestamp.tz_offset = tz_offset;
timestamp
});
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"after",
|_language, diagnostics, _build_ctx, self_property, function| {
let [date_pattern_node] = function.expect_exact_arguments()?;
let now = chrono::Local::now();
let date_pattern = template_parser::catch_aliases(
diagnostics,
date_pattern_node,
|_diagnostics, node| {
let date_pattern = template_parser::expect_string_literal(node)?;
DatePattern::from_str_kind(date_pattern, function.name, now).map_err(|err| {
TemplateParseError::expression("Invalid date pattern", node.span)
.with_source(err)
})
},
)?;
let out_property = self_property.map(move |timestamp| date_pattern.matches(&timestamp));
Ok(out_property.into_dyn_wrapped())
},
);
map.insert("before", map["after"]);
map.insert(
"since",
|language, diagnostics, build_ctx, self_property, function| {
let [date_node] = function.expect_exact_arguments()?;
let date_property =
expect_timestamp_expression(language, diagnostics, build_ctx, date_node)?;
let out_property =
(self_property, date_property).and_then(move |(self_timestamp, arg_timestamp)| {
Ok(TimestampRange {
start: arg_timestamp,
end: self_timestamp,
})
});
Ok(out_property.into_dyn_wrapped())
},
);
map
}
fn builtin_timestamp_range_methods<'a, L: TemplateLanguage<'a> + ?Sized>()
-> TemplateBuildMethodFnMap<'a, L, TimestampRange> {
// Not using maplit::hashmap!{} or custom declarative macro here because
// code completion inside macro is quite restricted.
let mut map = TemplateBuildMethodFnMap::<L, TimestampRange>::new();
map.insert(
"start",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.map(|time_range| time_range.start);
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"end",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.map(|time_range| time_range.end);
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"duration",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
// TODO: Introduce duration type, and move formatting to it.
let out_property = self_property.and_then(|time_range| {
let mut f = timeago::Formatter::new();
f.min_unit(timeago::TimeUnit::Microseconds).ago("");
let duration = time_util::format_duration(&time_range.start, &time_range.end, &f)?;
if duration == "now" {
Ok("less than a microsecond".to_owned())
} else {
Ok(duration)
}
});
Ok(out_property.into_dyn_wrapped())
},
);
map
}
fn builtin_any_list_methods<'a, L: TemplateLanguage<'a> + ?Sized>() -> BuildAnyMethodFnMap<'a, L> {
// Not using maplit::hashmap!{} or custom declarative macro here because
// code completion inside macro is quite restricted.
let mut map = BuildAnyMethodFnMap::<L>::new();
map.insert(
"join",
|language, diagnostics, build_ctx, self_template, function| {
let [separator_node] = function.expect_exact_arguments()?;
let separator =
expect_template_expression(language, diagnostics, build_ctx, separator_node)?;
Ok(L::Property::wrap_template(
self_template.try_join(separator).ok_or_else(|| {
// FIXME: This error should probably be reported on the type
// within the AnyListTemplateProperty.
TemplateParseError::expected_type("Template", "AnyList", function.name_span)
})?,
))
},
);
map
}
/// Creates new symbol table for printable list property.
pub fn builtin_formattable_list_methods<'a, L, O>() -> TemplateBuildMethodFnMap<'a, L, Vec<O>>
where
L: TemplateLanguage<'a> + ?Sized,
L::Property: WrapTemplateProperty<'a, O> + WrapTemplateProperty<'a, Vec<O>>,
O: Template + Clone + 'a,
{
let mut map = builtin_unformattable_list_methods::<L, O>();
map.insert(
"join",
|language, diagnostics, build_ctx, self_property, function| {
let [separator_node] = function.expect_exact_arguments()?;
let separator =
expect_template_expression(language, diagnostics, build_ctx, separator_node)?;
let template =
ListPropertyTemplate::new(self_property, separator, |formatter, item| {
item.format(formatter)
});
Ok(L::Property::wrap_template(Box::new(template)))
},
);
map
}
/// Creates new symbol table for unprintable list property.
pub fn builtin_unformattable_list_methods<'a, L, O>() -> TemplateBuildMethodFnMap<'a, L, Vec<O>>
where
L: TemplateLanguage<'a> + ?Sized,
L::Property: WrapTemplateProperty<'a, O> + WrapTemplateProperty<'a, Vec<O>>,
O: Clone + 'a,
{
// Not using maplit::hashmap!{} or custom declarative macro here because
// code completion inside macro is quite restricted.
let mut map = TemplateBuildMethodFnMap::<L, Vec<O>>::new();
map.insert(
"len",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.and_then(|items| Ok(i64::try_from(items.len())?));
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"filter",
|language, diagnostics, build_ctx, self_property, function| {
let out_property: BoxedTemplateProperty<'a, Vec<O>> =
build_filter_operation(language, diagnostics, build_ctx, self_property, function)?;
Ok(L::Property::wrap_property(out_property))
},
);
map.insert(
"map",
|language, diagnostics, build_ctx, self_property, function| {
let map_result =
build_map_operation(language, diagnostics, build_ctx, self_property, function)?;
Ok(L::Property::wrap_any_list(map_result))
},
);
map.insert(
"any",
|language, diagnostics, build_ctx, self_property, function| {
let out_property =
build_any_operation(language, diagnostics, build_ctx, self_property, function)?;
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"all",
|language, diagnostics, build_ctx, self_property, function| {
let out_property =
build_all_operation(language, diagnostics, build_ctx, self_property, function)?;
Ok(out_property.into_dyn_wrapped())
},
);
map.insert(
"first",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
// TODO: Return `Option<T>` instead of erroring out.
let out_property = self_property.and_then(|items| {
items
.into_iter()
.next()
.ok_or_else(|| TemplatePropertyError("List is empty".into()))
});
Ok(L::Property::wrap_property(out_property.into_dyn()))
},
);
map.insert(
"last",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
// TODO: Return `Option<T>` instead of erroring out.
let out_property = self_property.and_then(|mut items| {
items
.pop()
.ok_or_else(|| TemplatePropertyError("List is empty".into()))
});
Ok(L::Property::wrap_property(out_property.into_dyn()))
},
);
map.insert(
"get",
|language, diagnostics, build_ctx, self_property, function| {
let [index_node] = function.expect_exact_arguments()?;
let index = expect_usize_expression(language, diagnostics, build_ctx, index_node)?;
// TODO: Return `Option<T>` instead of erroring out.
let out_property = (self_property, index).and_then(|(mut items, index)| {
if index < items.len() {
Ok(items.remove(index))
} else {
Err(TemplatePropertyError(
format!("Index {index} out of bounds").into(),
))
}
});
Ok(L::Property::wrap_property(out_property.into_dyn()))
},
);
map.insert(
"reverse",
|_language, _diagnostics, _build_ctx, self_property, function| {
function.expect_no_arguments()?;
let out_property = self_property.map(|mut items| {
items.reverse();
items
});
Ok(L::Property::wrap_property(out_property.into_dyn()))
},
);
map.insert(
"skip",
|language, diagnostics, build_ctx, self_property, function| {
let [count_node] = function.expect_exact_arguments()?;
let count = expect_usize_expression(language, diagnostics, build_ctx, count_node)?;
let out_property = (self_property, count)
.map(|(items, count)| items.into_iter().skip(count).collect_vec());
Ok(L::Property::wrap_property(out_property.into_dyn()))
},
);
map.insert(
"take",
|language, diagnostics, build_ctx, self_property, function| {
let [count_node] = function.expect_exact_arguments()?;
let count = expect_usize_expression(language, diagnostics, build_ctx, count_node)?;
let out_property = (self_property, count)
.map(|(items, count)| items.into_iter().take(count).collect_vec());
Ok(L::Property::wrap_property(out_property.into_dyn()))
},
);
map
}
/// Builds expression that extracts iterable property and filters its items.
fn build_filter_operation<'a, L, O, P, B>(
language: &L,
diagnostics: &mut TemplateDiagnostics,
build_ctx: &BuildContext<L::Property>,
self_property: P,
function: &FunctionCallNode,
) -> TemplateParseResult<BoxedTemplateProperty<'a, B>>
where
L: TemplateLanguage<'a> + ?Sized,
L::Property: WrapTemplateProperty<'a, O>,
P: TemplateProperty + 'a,
P::Output: IntoIterator<Item = O>,
O: Clone + 'a,
B: FromIterator<O>,
{
let [lambda_node] = function.expect_exact_arguments()?;
let item_placeholder = PropertyPlaceholder::new();
let item_predicate =
template_parser::catch_aliases(diagnostics, lambda_node, |diagnostics, node| {
let lambda = template_parser::expect_lambda(node)?;
build_lambda_expression(
build_ctx,
lambda,
&[&|| item_placeholder.clone().into_dyn_wrapped()],
|build_ctx, body| expect_boolean_expression(language, diagnostics, build_ctx, body),
)
})?;
let out_property = self_property.and_then(move |items| {
items
.into_iter()
.filter_map(|item| {
// Evaluate predicate with the current item
item_placeholder.set(item);
let result = item_predicate.extract();
let item = item_placeholder.take().unwrap();
result.map(|pred| pred.then_some(item)).transpose()
})
.collect()
});
Ok(out_property.into_dyn())
}
/// Builds expression that extracts iterable property and applies template to
/// each item.
fn build_map_operation<'a, L, O, P>(
language: &L,
diagnostics: &mut TemplateDiagnostics,
build_ctx: &BuildContext<L::Property>,
self_property: P,
function: &FunctionCallNode,
) -> TemplateParseResult<BoxedAnyProperty<'a>>
where
L: TemplateLanguage<'a> + ?Sized,
L::Property: WrapTemplateProperty<'a, O>,
P: TemplateProperty + 'a,
P::Output: IntoIterator<Item = O>,
O: Clone + 'a,
{
let [lambda_node] = function.expect_exact_arguments()?;
let item_placeholder = PropertyPlaceholder::new();
let mapped_item =
template_parser::catch_aliases(diagnostics, lambda_node, |diagnostics, node| {
let lambda = template_parser::expect_lambda(node)?;
build_lambda_expression(
build_ctx,
lambda,
&[&|| item_placeholder.clone().into_dyn_wrapped()],
|build_ctx, body| expect_any_expression(language, diagnostics, build_ctx, body),
)
})?;
let mapped_list = ListMapProperty::new(self_property, item_placeholder, mapped_item);
Ok(Box::new(mapped_list))
}
/// Builds expression that checks if any item in the list satisfies the
/// predicate.
fn build_any_operation<'a, L, O, P>(
language: &L,
diagnostics: &mut TemplateDiagnostics,
build_ctx: &BuildContext<L::Property>,
self_property: P,
function: &FunctionCallNode,
) -> TemplateParseResult<BoxedTemplateProperty<'a, bool>>
where
L: TemplateLanguage<'a> + ?Sized,
L::Property: WrapTemplateProperty<'a, O>,
P: TemplateProperty + 'a,
P::Output: IntoIterator<Item = O>,
O: Clone + 'a,
{
let [lambda_node] = function.expect_exact_arguments()?;
let item_placeholder = PropertyPlaceholder::new();
let item_predicate =
template_parser::catch_aliases(diagnostics, lambda_node, |diagnostics, node| {
let lambda = template_parser::expect_lambda(node)?;
build_lambda_expression(
build_ctx,
lambda,
&[&|| item_placeholder.clone().into_dyn_wrapped()],
|build_ctx, body| expect_boolean_expression(language, diagnostics, build_ctx, body),
)
})?;
let out_property = self_property.and_then(move |items| {
items
.into_iter()
.map(|item| item_placeholder.with_value(item, || item_predicate.extract()))
.process_results(|mut predicates| predicates.any(|p| p))
});
Ok(out_property.into_dyn())
}
/// Builds expression that checks if all items in the list satisfy the
/// predicate.
fn build_all_operation<'a, L, O, P>(
language: &L,
diagnostics: &mut TemplateDiagnostics,
build_ctx: &BuildContext<L::Property>,
self_property: P,
function: &FunctionCallNode,
) -> TemplateParseResult<BoxedTemplateProperty<'a, bool>>
where
L: TemplateLanguage<'a> + ?Sized,
L::Property: WrapTemplateProperty<'a, O>,
P: TemplateProperty + 'a,
P::Output: IntoIterator<Item = O>,
O: Clone + 'a,
{
let [lambda_node] = function.expect_exact_arguments()?;
let item_placeholder = PropertyPlaceholder::new();
let item_predicate =
template_parser::catch_aliases(diagnostics, lambda_node, |diagnostics, node| {
let lambda = template_parser::expect_lambda(node)?;
build_lambda_expression(
build_ctx,
lambda,
&[&|| item_placeholder.clone().into_dyn_wrapped()],
|build_ctx, body| expect_boolean_expression(language, diagnostics, build_ctx, body),
)
})?;
let out_property = self_property.and_then(move |items| {
items
.into_iter()
.map(|item| item_placeholder.with_value(item, || item_predicate.extract()))
.process_results(|mut predicates| predicates.all(|p| p))
});
Ok(out_property.into_dyn())
}
/// Builds lambda expression to be evaluated with the provided arguments.
/// `arg_fns` is usually an array of wrapped [`PropertyPlaceholder`]s.
fn build_lambda_expression<'i, P, T>(
build_ctx: &BuildContext<'i, P>,
lambda: &LambdaNode<'i>,
arg_fns: &[&'i dyn Fn() -> P],
build_body: impl FnOnce(&BuildContext<'i, P>, &ExpressionNode<'i>) -> TemplateParseResult<T>,
) -> TemplateParseResult<T> {
if lambda.params.len() != arg_fns.len() {
return Err(TemplateParseError::expression(
format!("Expected {} lambda parameters", arg_fns.len()),
lambda.params_span,
));
}
let mut local_variables = build_ctx.local_variables.clone();
local_variables.extend(iter::zip(&lambda.params, arg_fns));
let inner_build_ctx = BuildContext {
local_variables,
self_variable: build_ctx.self_variable,
};
build_body(&inner_build_ctx, &lambda.body)
}
fn builtin_functions<'a, L: TemplateLanguage<'a> + ?Sized>() -> TemplateBuildFunctionFnMap<'a, L> {
// Not using maplit::hashmap!{} or custom declarative macro here because
// code completion inside macro is quite restricted.
let mut map = TemplateBuildFunctionFnMap::<L>::new();
map.insert("fill", |language, diagnostics, build_ctx, function| {
let [width_node, content_node] = function.expect_exact_arguments()?;
let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
let template =
ReformatTemplate::new(content, move |formatter, recorded| match width.extract() {
Ok(width) => text_util::write_wrapped(formatter.as_mut(), recorded, width),
Err(err) => formatter.handle_error(err),
});
Ok(L::Property::wrap_template(Box::new(template)))
});
map.insert("indent", |language, diagnostics, build_ctx, function| {
let [prefix_node, content_node] = function.expect_exact_arguments()?;
let prefix = expect_template_expression(language, diagnostics, build_ctx, prefix_node)?;
let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
let template = ReformatTemplate::new(content, move |formatter, recorded| {
let rewrap = formatter.rewrap_fn();
text_util::write_indented(formatter.as_mut(), recorded, |formatter| {
prefix.format(&mut rewrap(formatter))
})
});
Ok(L::Property::wrap_template(Box::new(template)))
});
map.insert("pad_start", |language, diagnostics, build_ctx, function| {
let ([width_node, content_node], [fill_char_node]) =
function.expect_named_arguments(&["", "", "fill_char"])?;
let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
let fill_char = fill_char_node
.map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
.transpose()?;
let template = new_pad_template(content, fill_char, width, text_util::write_padded_start);
Ok(L::Property::wrap_template(template))
});
map.insert("pad_end", |language, diagnostics, build_ctx, function| {
let ([width_node, content_node], [fill_char_node]) =
function.expect_named_arguments(&["", "", "fill_char"])?;
let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
let fill_char = fill_char_node
.map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
.transpose()?;
let template = new_pad_template(content, fill_char, width, text_util::write_padded_end);
Ok(L::Property::wrap_template(template))
});
map.insert(
"pad_centered",
|language, diagnostics, build_ctx, function| {
let ([width_node, content_node], [fill_char_node]) =
function.expect_named_arguments(&["", "", "fill_char"])?;
let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
let content =
expect_template_expression(language, diagnostics, build_ctx, content_node)?;
let fill_char = fill_char_node
.map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
.transpose()?;
let template =
new_pad_template(content, fill_char, width, text_util::write_padded_centered);
Ok(L::Property::wrap_template(template))
},
);
map.insert(
"truncate_start",
|language, diagnostics, build_ctx, function| {
let ([width_node, content_node], [ellipsis_node]) =
function.expect_named_arguments(&["", "", "ellipsis"])?;
let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
let content =
expect_template_expression(language, diagnostics, build_ctx, content_node)?;
let ellipsis = ellipsis_node
.map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
.transpose()?;
let template =
new_truncate_template(content, ellipsis, width, text_util::write_truncated_start);
Ok(L::Property::wrap_template(template))
},
);
map.insert(
"truncate_end",
|language, diagnostics, build_ctx, function| {
let ([width_node, content_node], [ellipsis_node]) =
function.expect_named_arguments(&["", "", "ellipsis"])?;
let width = expect_usize_expression(language, diagnostics, build_ctx, width_node)?;
let content =
expect_template_expression(language, diagnostics, build_ctx, content_node)?;
let ellipsis = ellipsis_node
.map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
.transpose()?;
let template =
new_truncate_template(content, ellipsis, width, text_util::write_truncated_end);
Ok(L::Property::wrap_template(template))
},
);
map.insert("hash", |language, diagnostics, build_ctx, function| {
let [content_node] = function.expect_exact_arguments()?;
let content = expect_stringify_expression(language, diagnostics, build_ctx, content_node)?;
let result = content.map(|c| hex_util::encode_hex(blake2b_hash(&c).as_ref()));
Ok(result.into_dyn_wrapped())
});
map.insert("label", |language, diagnostics, build_ctx, function| {
let [label_node, content_node] = function.expect_exact_arguments()?;
let label_property =
expect_stringify_expression(language, diagnostics, build_ctx, label_node)?;
let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
let labels =
label_property.map(|s| s.split_whitespace().map(ToString::to_string).collect());
Ok(L::Property::wrap_template(Box::new(LabelTemplate::new(
content, labels,
))))
});
map.insert(
"raw_escape_sequence",
|language, diagnostics, build_ctx, function| {
let [content_node] = function.expect_exact_arguments()?;
let content =
expect_template_expression(language, diagnostics, build_ctx, content_node)?;
Ok(L::Property::wrap_template(Box::new(
RawEscapeSequenceTemplate(content),
)))
},
);
map.insert("hyperlink", |language, diagnostics, build_ctx, function| {
let ([url_node, text_node], [fallback_node]) = function.expect_arguments()?;
let url = expect_stringify_expression(language, diagnostics, build_ctx, url_node)?;
let text = expect_template_expression(language, diagnostics, build_ctx, text_node)?;
let fallback = fallback_node
.map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
.transpose()?;
Ok(L::Property::wrap_template(Box::new(
HyperlinkTemplate::new(url, text, fallback),
)))
});
map.insert("replace", |language, diagnostics, build_ctx, function| {
let ([pattern_node, content_node, replacement_lambda_node], []) =
function.expect_arguments()?;
let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
let pattern = template_parser::expect_string_pattern(pattern_node)?;
let regex = pattern.to_regex();
let captures_placeholder = PropertyPlaceholder::new();
let replacement_template = template_parser::catch_aliases(
diagnostics,
replacement_lambda_node,
|diagnostics, node| {
let lambda = template_parser::expect_lambda(node)?;
build_lambda_expression(
build_ctx,
lambda,
&[&|| captures_placeholder.clone().into_dyn_wrapped()],
|build_ctx, body| {
expect_template_expression(language, diagnostics, build_ctx, body)
},
)
},
)?;
let template = new_replace_template(
content,
regex.clone(),
captures_placeholder.clone(),
move |formatter| replacement_template.format(formatter),
);
Ok(L::Property::wrap_template(template))
});
map.insert("stringify", |language, diagnostics, build_ctx, function| {
let [content_node] = function.expect_exact_arguments()?;
let content = expect_stringify_expression(language, diagnostics, build_ctx, content_node)?;
Ok(L::Property::wrap_property(content))
});
map.insert("json", |language, diagnostics, build_ctx, function| {
// TODO: Add pretty=true|false? or json(key=value, ..)? The latter might
// be implemented as a map constructor/literal if we add support for
// heterogeneous list/map types.
let [value_node] = function.expect_exact_arguments()?;
let value = expect_serialize_expression(language, diagnostics, build_ctx, value_node)?;
let out_property = value.and_then(|v| Ok(serde_json::to_string(&v)?));
Ok(out_property.into_dyn_wrapped())
});
map.insert("if", |language, diagnostics, build_ctx, function| {
let ([condition_node, true_node], [false_node]) = function.expect_arguments()?;
let condition =
expect_boolean_expression(language, diagnostics, build_ctx, condition_node)?;
let true_any = expect_any_expression(language, diagnostics, build_ctx, true_node)?;
let false_any = false_node
.map(|node| expect_any_expression(language, diagnostics, build_ctx, node))
.transpose()?;
let property = ConditionalProperty::new(condition, true_any, false_any);
Ok(L::Property::wrap_any(Box::new(property)))
});
map.insert("coalesce", |language, diagnostics, build_ctx, function| {
let ([], content_nodes) = function.expect_some_arguments()?;
let contents = content_nodes
.iter()
.map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
.try_collect()?;
Ok(L::Property::wrap_template(Box::new(CoalesceTemplate(
contents,
))))
});
map.insert("concat", |language, diagnostics, build_ctx, function| {
let ([], content_nodes) = function.expect_some_arguments()?;
let contents = content_nodes
.iter()
.map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
.try_collect()?;
Ok(L::Property::wrap_template(Box::new(ConcatTemplate(
contents,
))))
});
map.insert("join", |language, diagnostics, build_ctx, function| {
let ([separator_node], content_nodes) = function.expect_some_arguments()?;
let separator =
expect_template_expression(language, diagnostics, build_ctx, separator_node)?;
let contents = content_nodes
.iter()
.map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
.try_collect()?;
Ok(L::Property::wrap_template(Box::new(JoinTemplate::new(
separator, contents,
))))
});
map.insert("separate", |language, diagnostics, build_ctx, function| {
let ([separator_node], content_nodes) = function.expect_some_arguments()?;
let separator =
expect_template_expression(language, diagnostics, build_ctx, separator_node)?;
let contents = content_nodes
.iter()
.map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
.try_collect()?;
Ok(L::Property::wrap_template(Box::new(SeparateTemplate::new(
separator, contents,
))))
});
map.insert("surround", |language, diagnostics, build_ctx, function| {
let [prefix_node, suffix_node, content_node] = function.expect_exact_arguments()?;
let prefix = expect_template_expression(language, diagnostics, build_ctx, prefix_node)?;
let suffix = expect_template_expression(language, diagnostics, build_ctx, suffix_node)?;
let content = expect_template_expression(language, diagnostics, build_ctx, content_node)?;
let template = ReformatTemplate::new(content, move |formatter, recorded| {
if recorded.data().is_empty() {
return Ok(());
}
prefix.format(formatter)?;
recorded.replay(formatter.as_mut())?;
suffix.format(formatter)?;
Ok(())
});
Ok(L::Property::wrap_template(Box::new(template)))
});
map.insert("config", |language, diagnostics, build_ctx, function| {
let [name_node] = function.expect_exact_arguments()?;
let name_expression =
expect_stringify_expression(language, diagnostics, build_ctx, name_node)?;
if let Ok(name) = name_expression.extract() {
let config_path: ConfigNamePathBuf =
template_parser::catch_aliases(diagnostics, name_node, |_diagnostics, node| {
name.parse().map_err(|err| {
TemplateParseError::expression("Failed to parse config name", node.span)
.with_source(err)
})
})?;
let value = language
.settings()
.get_value(config_path)
.optional()
.map_err(|err| {
TemplateParseError::expression("Failed to get config value", function.name_span)
.with_source(err)
})?;
// .decorated("", "") to trim leading/trailing whitespace
Ok(Literal(value.map(|v| v.decorated("", ""))).into_dyn_wrapped())
} else {
let settings = language.settings().clone();
let out_property = name_expression.and_then(move |name| {
let config_path: ConfigNamePathBuf = name.parse()?;
let value = settings.get_value(config_path).optional()?;
Ok(value.map(|v| v.decorated("", "")))
});
Ok(out_property.into_dyn_wrapped())
}
});
map
}
fn new_pad_template<'a, W>(
content: Box<dyn Template + 'a>,
fill_char: Option<Box<dyn Template + 'a>>,
width: BoxedTemplateProperty<'a, usize>,
write_padded: W,
) -> Box<dyn Template + 'a>
where
W: Fn(&mut dyn Formatter, &FormatRecorder, &FormatRecorder, usize) -> io::Result<()> + 'a,
{
let default_fill_char = FormatRecorder::with_data(" ");
let template = ReformatTemplate::new(content, move |formatter, recorded| {
let width = match width.extract() {
Ok(width) => width,
Err(err) => return formatter.handle_error(err),
};
let mut fill_char_recorder;
let recorded_fill_char = if let Some(fill_char) = &fill_char {
let rewrap = formatter.rewrap_fn();
fill_char_recorder = FormatRecorder::new(formatter.maybe_color());
fill_char.format(&mut rewrap(&mut fill_char_recorder))?;
&fill_char_recorder
} else {
&default_fill_char
};
write_padded(formatter.as_mut(), recorded, recorded_fill_char, width)
});
Box::new(template)
}
fn new_truncate_template<'a, W>(
content: Box<dyn Template + 'a>,
ellipsis: Option<Box<dyn Template + 'a>>,
width: BoxedTemplateProperty<'a, usize>,
write_truncated: W,
) -> Box<dyn Template + 'a>
where
W: Fn(&mut dyn Formatter, &FormatRecorder, &FormatRecorder, usize) -> io::Result<usize> + 'a,
{
let default_ellipsis = FormatRecorder::with_data("");
let template = ReformatTemplate::new(content, move |formatter, recorded| {
let width = match width.extract() {
Ok(width) => width,
Err(err) => return formatter.handle_error(err),
};
let mut ellipsis_recorder;
let recorded_ellipsis = if let Some(ellipsis) = &ellipsis {
let rewrap = formatter.rewrap_fn();
ellipsis_recorder = FormatRecorder::new(formatter.maybe_color());
ellipsis.format(&mut rewrap(&mut ellipsis_recorder))?;
&ellipsis_recorder
} else {
&default_ellipsis
};
write_truncated(formatter.as_mut(), recorded, recorded_ellipsis, width)?;
Ok(())
});
Box::new(template)
}
fn new_replace_template<'a, W>(
content: Box<dyn Template + 'a>,
regex: regex::bytes::Regex,
captures_placeholder: PropertyPlaceholder<RegexCaptures>,
write_replacement_content: W,
) -> Box<dyn Template + 'a>
where
W: Fn(&mut TemplateFormatter) -> io::Result<()> + 'a,
{
// Build named capture group map once from the regex.
let names_map: HashMap<String, usize> = regex
.capture_names()
.enumerate()
.filter_map(|(i, name)| name.map(|n| (n.to_string(), i)))
.collect();
let template = ReformatTemplate::new(content, move |formatter, recorded| {
let data = recorded.data();
let mut recorded_replacements = vec![];
let mut content_ranges = vec![];
for captures in regex.captures_iter(data) {
let full_match = captures.get(0).expect("capture group 0 is always present");
content_ranges.push(full_match.range());
let capture_ranges = captures
.iter()
.map(|m| m.map(|m| m.range()).unwrap_or_default())
.collect_vec();
captures_placeholder.with_value(
RegexCaptures::new(data.to_vec(), capture_ranges, names_map.clone()),
|| -> io::Result<()> {
let mut recorder = FormatRecorder::new(formatter.maybe_color());
let rewrap = formatter.rewrap_fn();
write_replacement_content(&mut rewrap(&mut recorder))?;
recorded_replacements.push(recorder);
Ok(())
},
)?;
}
write_replaced(
formatter.as_mut(),
recorded,
&content_ranges,
|formatter, index| recorded_replacements[index].replay(formatter),
)
});
Box::new(template)
}
/// Builds intermediate expression tree from AST nodes.
pub fn build_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
language: &L,
diagnostics: &mut TemplateDiagnostics,
build_ctx: &BuildContext<L::Property>,
node: &ExpressionNode,
) -> TemplateParseResult<Expression<L::Property>> {
template_parser::catch_aliases(diagnostics, node, |diagnostics, node| match &node.kind {
ExpressionKind::Identifier(name) => {
if let Some(make) = build_ctx.local_variables.get(name) {
// Don't label a local variable with its name
Ok(Expression::unlabeled(make()))
} else if *name == "self" {
// "self" is a special variable, so don't label it
let make = build_ctx.self_variable;
Ok(Expression::unlabeled(make()))
} else {
let property = build_keyword(language, diagnostics, build_ctx, name, node.span)
.map_err(|err| {
err.extend_keyword_candidates(itertools::chain(
build_ctx.local_variables.keys().copied(),
["self"],
))
})?;
Ok(Expression::with_label(property, *name))
}
}
ExpressionKind::Boolean(value) => {
let property = Literal(*value).into_dyn_wrapped();
Ok(Expression::unlabeled(property))
}
ExpressionKind::Integer(value) => {
let property = Literal(*value).into_dyn_wrapped();
Ok(Expression::unlabeled(property))
}
ExpressionKind::String(value) => {
let property = Literal(value.clone()).into_dyn_wrapped();
Ok(Expression::unlabeled(property))
}
ExpressionKind::Pattern(_) => Err(TemplateParseError::expression(
"String patterns may not be used as expression values",
node.span,
)),
ExpressionKind::Unary(op, arg_node) => {
let property = build_unary_operation(language, diagnostics, build_ctx, *op, arg_node)?;
Ok(Expression::unlabeled(property))
}
ExpressionKind::Binary(op, lhs_node, rhs_node) => {
let property = build_binary_operation(
language,
diagnostics,
build_ctx,
*op,
lhs_node,
rhs_node,
node.span,
)?;
Ok(Expression::unlabeled(property))
}
ExpressionKind::Concat(nodes) => {
let templates = nodes
.iter()
.map(|node| expect_template_expression(language, diagnostics, build_ctx, node))
.try_collect()?;
let property = L::Property::wrap_template(Box::new(ConcatTemplate(templates)));
Ok(Expression::unlabeled(property))
}
ExpressionKind::FunctionCall(function) => {
let property = language.build_function(diagnostics, build_ctx, function)?;
Ok(Expression::unlabeled(property))
}
ExpressionKind::MethodCall(method) => {
let mut expression =
build_expression(language, diagnostics, build_ctx, &method.object)?;
expression.property = language.build_method(
diagnostics,
build_ctx,
expression.property,
&method.function,
)?;
expression.labels.push(method.function.name.to_owned());
Ok(expression)
}
ExpressionKind::Lambda(_) => Err(TemplateParseError::expression(
"Lambda cannot be defined here",
node.span,
)),
ExpressionKind::AliasExpanded(..) => unreachable!(),
})
}
/// Builds template evaluation tree from AST nodes, with fresh build context.
pub fn build<'a, C, L>(
language: &L,
diagnostics: &mut TemplateDiagnostics,
node: &ExpressionNode,
) -> TemplateParseResult<TemplateRenderer<'a, C>>
where
C: Clone + 'a,
L: TemplateLanguage<'a> + ?Sized,
L::Property: WrapTemplateProperty<'a, C>,
{
let self_placeholder = PropertyPlaceholder::new();
let build_ctx = BuildContext {
local_variables: HashMap::new(),
self_variable: &|| self_placeholder.clone().into_dyn_wrapped(),
};
let template = expect_template_expression(language, diagnostics, &build_ctx, node)?;
Ok(TemplateRenderer::new(template, self_placeholder))
}
/// Parses text, expands aliases, then builds template evaluation tree.
pub fn parse<'a, C, L>(
language: &L,
diagnostics: &mut TemplateDiagnostics,
template_text: &str,
aliases_map: &TemplateAliasesMap,
) -> TemplateParseResult<TemplateRenderer<'a, C>>
where
C: Clone + 'a,
L: TemplateLanguage<'a> + ?Sized,
L::Property: WrapTemplateProperty<'a, C>,
{
let node = template_parser::parse(template_text, aliases_map)?;
build(language, diagnostics, &node).map_err(|err| err.extend_alias_candidates(aliases_map))
}
pub fn expect_boolean_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
language: &L,
diagnostics: &mut TemplateDiagnostics,
build_ctx: &BuildContext<L::Property>,
node: &ExpressionNode,
) -> TemplateParseResult<BoxedTemplateProperty<'a, bool>> {
expect_expression_of_type(
language,
diagnostics,
build_ctx,
node,
"Boolean",
|expression| expression.try_into_boolean(),
)
}
pub fn expect_integer_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
language: &L,
diagnostics: &mut TemplateDiagnostics,
build_ctx: &BuildContext<L::Property>,
node: &ExpressionNode,
) -> TemplateParseResult<BoxedTemplateProperty<'a, i64>> {
expect_expression_of_type(
language,
diagnostics,
build_ctx,
node,
"Integer",
|expression| expression.try_into_integer(),
)
}
/// If the given expression `node` is of `Integer` type, converts it to `isize`.
pub fn expect_isize_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
language: &L,
diagnostics: &mut TemplateDiagnostics,
build_ctx: &BuildContext<L::Property>,
node: &ExpressionNode,
) -> TemplateParseResult<BoxedTemplateProperty<'a, isize>> {
let i64_property = expect_integer_expression(language, diagnostics, build_ctx, node)?;
let isize_property = i64_property.and_then(|v| Ok(isize::try_from(v)?));
Ok(isize_property.into_dyn())
}
/// If the given expression `node` is of `Integer` type, converts it to `usize`.
pub fn expect_usize_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
language: &L,
diagnostics: &mut TemplateDiagnostics,
build_ctx: &BuildContext<L::Property>,
node: &ExpressionNode,
) -> TemplateParseResult<BoxedTemplateProperty<'a, usize>> {
let i64_property = expect_integer_expression(language, diagnostics, build_ctx, node)?;
let usize_property = i64_property.and_then(|v| Ok(usize::try_from(v)?));
Ok(usize_property.into_dyn())
}
pub fn expect_byte_stringify_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
language: &L,
diagnostics: &mut TemplateDiagnostics,
build_ctx: &BuildContext<L::Property>,
node: &ExpressionNode,
) -> TemplateParseResult<BoxedTemplateProperty<'a, BString>> {
// Since any formattable type can be converted to a string property, the
// expected type is not a ByteString.
expect_expression_of_type(
language,
diagnostics,
build_ctx,
node,
"ByteStringify",
|expression| expression.try_into_byte_stringify(),
)
}
pub fn expect_stringify_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
language: &L,
diagnostics: &mut TemplateDiagnostics,
build_ctx: &BuildContext<L::Property>,
node: &ExpressionNode,
) -> TemplateParseResult<BoxedTemplateProperty<'a, String>> {
// Since any formattable type can be converted to a string property, the
// expected type is not a String.
expect_expression_of_type(
language,
diagnostics,
build_ctx,
node,
"Stringify",
|expression| expression.try_into_stringify(),
)
}
pub fn expect_timestamp_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
language: &L,
diagnostics: &mut TemplateDiagnostics,
build_ctx: &BuildContext<L::Property>,
node: &ExpressionNode,
) -> TemplateParseResult<BoxedTemplateProperty<'a, Timestamp>> {
expect_expression_of_type(
language,
diagnostics,
build_ctx,
node,
"Timestamp",
|expression| expression.try_into_timestamp(),
)
}
pub fn expect_serialize_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
language: &L,
diagnostics: &mut TemplateDiagnostics,
build_ctx: &BuildContext<L::Property>,
node: &ExpressionNode,
) -> TemplateParseResult<BoxedSerializeProperty<'a>> {
expect_expression_of_type(
language,
diagnostics,
build_ctx,
node,
"Serialize",
|expression| expression.try_into_serialize(),
)
}
pub fn expect_template_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
language: &L,
diagnostics: &mut TemplateDiagnostics,
build_ctx: &BuildContext<L::Property>,
node: &ExpressionNode,
) -> TemplateParseResult<Box<dyn Template + 'a>> {
expect_expression_of_type(
language,
diagnostics,
build_ctx,
node,
"Template",
|expression| expression.try_into_template(),
)
}
pub fn expect_any_expression<'a, L: TemplateLanguage<'a> + ?Sized>(
language: &L,
diagnostics: &mut TemplateDiagnostics,
build_ctx: &BuildContext<L::Property>,
node: &ExpressionNode,
) -> TemplateParseResult<BoxedAnyProperty<'a>> {
template_parser::catch_aliases(diagnostics, node, |diagnostics, node| {
Ok(
Box::new(build_expression(language, diagnostics, build_ctx, node)?)
as BoxedAnyProperty<'a>,
)
})
}
fn expect_expression_of_type<'a, L: TemplateLanguage<'a> + ?Sized, T>(
language: &L,
diagnostics: &mut TemplateDiagnostics,
build_ctx: &BuildContext<L::Property>,
node: &ExpressionNode,
expected_type: &str,
f: impl FnOnce(Expression<L::Property>) -> Option<T>,
) -> TemplateParseResult<T> {
template_parser::catch_aliases(diagnostics, node, |diagnostics, node| {
let expression = build_expression(language, diagnostics, build_ctx, node)?;
let actual_type = expression.type_name();
f(expression)
.ok_or_else(|| TemplateParseError::expected_type(expected_type, actual_type, node.span))
})
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use jj_lib::backend::MillisSinceEpoch;
use jj_lib::config::StackedConfig;
use super::*;
use crate::formatter;
use crate::formatter::ColorFormatter;
use crate::generic_templater;
use crate::generic_templater::GenericTemplateLanguage;
#[derive(Clone, Debug, serde::Serialize)]
struct Context;
type TestTemplateLanguage = GenericTemplateLanguage<'static, Context>;
type TestTemplatePropertyKind = <TestTemplateLanguage as TemplateLanguage<'static>>::Property;
generic_templater::impl_self_property_wrapper!(Context);
/// Helper to set up template evaluation environment.
struct TestTemplateEnv {
language: TestTemplateLanguage,
aliases_map: TemplateAliasesMap,
color_rules: Vec<(Vec<String>, formatter::Style)>,
}
impl TestTemplateEnv {
fn new() -> Self {
Self::with_config(StackedConfig::with_defaults())
}
fn with_config(config: StackedConfig) -> Self {
let settings = UserSettings::from_config(config).unwrap();
Self {
language: TestTemplateLanguage::new(&settings),
aliases_map: TemplateAliasesMap::new(),
color_rules: Vec::new(),
}
}
}
impl TestTemplateEnv {
fn add_keyword<F>(&mut self, name: &'static str, build: F)
where
F: Fn() -> TestTemplatePropertyKind + 'static,
{
self.language.add_keyword(name, move |_| Ok(build()));
}
/// Like `add_keyword`, but the value depends on the `self` context
/// property, making it not statically extractable.
fn add_dynamic_keyword<O, F>(&mut self, name: &'static str, build: F)
where
O: 'static,
F: Fn() -> O + Clone + 'static,
TestTemplatePropertyKind: WrapTemplateProperty<'static, O>,
{
self.language.add_keyword(name, move |self_property| {
let build = build.clone();
Ok(self_property.map(move |_| build()).into_dyn_wrapped())
});
}
fn add_alias(&mut self, decl: impl AsRef<str>, defn: impl Into<String>) {
self.aliases_map.insert(decl, defn, None).unwrap();
}
fn add_color(&mut self, label: &str, fg: crossterm::style::Color) {
let labels = label.split_whitespace().map(|s| s.to_owned()).collect();
let style = formatter::Style {
fg: Some(fg),
..Default::default()
};
self.color_rules.push((labels, style));
}
fn parse(&self, template: &str) -> TemplateParseResult<TemplateRenderer<'static, Context>> {
parse(
&self.language,
&mut TemplateDiagnostics::new(),
template,
&self.aliases_map,
)
}
fn parse_err(&self, template: &str) -> String {
let err = self
.parse(template)
.err()
.expect("Got unexpected successful template rendering");
iter::successors(Some(&err as &dyn std::error::Error), |e| e.source()).join("\n")
}
fn parse_err_kind(&self, template: &str) -> TemplateParseErrorKind {
self.parse(template)
.err()
.expect("Got unexpected successful template rendering")
.kind()
.clone()
}
fn render_ok(&self, template: &str) -> BString {
let template = self.parse(template).unwrap();
let mut output = Vec::new();
let mut formatter =
ColorFormatter::new(&mut output, self.color_rules.clone().into(), false);
template.format(&Context, &mut formatter).unwrap();
drop(formatter);
output.into()
}
fn render_plain(&self, template: &str) -> BString {
let template = self.parse(template).unwrap();
template.format_plain_text(&Context).into()
}
}
fn literal<'a, O>(value: O) -> TestTemplatePropertyKind
where
O: Clone + 'a,
TestTemplatePropertyKind: WrapTemplateProperty<'a, O>,
{
Literal(value).into_dyn_wrapped()
}
fn new_error_property<'a, O>(message: &'a str) -> TestTemplatePropertyKind
where
TestTemplatePropertyKind: WrapTemplateProperty<'a, O>,
{
Literal(())
.and_then(|()| Err(TemplatePropertyError(message.into())))
.into_dyn_wrapped()
}
fn new_signature(name: &str, email: &str) -> Signature {
Signature {
name: name.to_owned(),
email: email.to_owned(),
timestamp: new_timestamp(0, 0),
}
}
fn new_timestamp(msec: i64, tz_offset: i32) -> Timestamp {
Timestamp {
timestamp: MillisSinceEpoch(msec),
tz_offset,
}
}
#[test]
fn test_parsed_tree() {
let mut env = TestTemplateEnv::new();
env.add_keyword("divergent", || literal(false));
env.add_keyword("empty", || literal(true));
env.add_keyword("hello", || literal("Hello".to_owned()));
// Empty
insta::assert_snapshot!(env.render_ok(r#" "#), @"");
// Single term with whitespace
insta::assert_snapshot!(env.render_ok(r#" hello.upper() "#), @"HELLO");
// Multiple terms
insta::assert_snapshot!(env.render_ok(r#" hello.upper() ++ true "#), @"HELLOtrue");
// Parenthesized single term
insta::assert_snapshot!(env.render_ok(r#"(hello.upper())"#), @"HELLO");
// Parenthesized multiple terms and concatenation
insta::assert_snapshot!(env.render_ok(r#"(hello.upper() ++ " ") ++ empty"#), @"HELLO true");
// Parenthesized "if" condition
insta::assert_snapshot!(env.render_ok(r#"if((divergent), "t", "f")"#), @"f");
// Parenthesized method chaining
insta::assert_snapshot!(env.render_ok(r#"(hello).upper()"#), @"HELLO");
// Multi-line method chaining
insta::assert_snapshot!(env.render_ok("hello\n .upper()"), @"HELLO");
}
#[test]
fn test_parse_error() {
let mut env = TestTemplateEnv::new();
env.add_keyword("description", || literal("".to_owned()));
env.add_keyword("empty", || literal(true));
insta::assert_snapshot!(env.parse_err(r#"foo bar"#), @"
--> 1:5
|
1 | foo bar
| ^---
|
= expected <EOI>, `++`, `||`, `&&`, `==`, `!=`, `>=`, `>`, `<=`, `<`, `+`, `-`, `*`, `/`, or `%`
");
insta::assert_snapshot!(env.parse_err("1 +"), @"
--> 1:4
|
1 | 1 +
| ^---
|
= expected `!`, `-`, or <primary>
");
insta::assert_snapshot!(env.parse_err("self.timestamp"), @"
--> 1:6
|
1 | self.timestamp
| ^---
|
= expected <function>
");
insta::assert_snapshot!(env.parse_err(r#"foo"#), @"
--> 1:1
|
1 | foo
| ^-^
|
= Keyword `foo` doesn't exist
");
insta::assert_snapshot!(env.parse_err(r#"foo()"#), @"
--> 1:1
|
1 | foo()
| ^-^
|
= Function `foo` doesn't exist
");
insta::assert_snapshot!(env.parse_err(r#"false()"#), @"
--> 1:1
|
1 | false()
| ^---^
|
= Expected identifier
");
insta::assert_snapshot!(env.parse_err(r#"!foo"#), @"
--> 1:2
|
1 | !foo
| ^-^
|
= Keyword `foo` doesn't exist
");
insta::assert_snapshot!(env.parse_err(r#"true && 123"#), @"
--> 1:9
|
1 | true && 123
| ^-^
|
= Expected expression of type `Boolean`, but actual type is `Integer`
");
insta::assert_snapshot!(env.parse_err(r#"true == 1"#), @"
--> 1:1
|
1 | true == 1
| ^-------^
|
= Cannot compare expressions of type `Boolean` and `Integer`
");
insta::assert_snapshot!(env.parse_err(r#"true != 'a'"#), @"
--> 1:1
|
1 | true != 'a'
| ^---------^
|
= Cannot compare expressions of type `Boolean` and `String`
");
insta::assert_snapshot!(env.parse_err(r#"1 == true"#), @"
--> 1:1
|
1 | 1 == true
| ^-------^
|
= Cannot compare expressions of type `Integer` and `Boolean`
");
insta::assert_snapshot!(env.parse_err(r#"1 != 'a'"#), @"
--> 1:1
|
1 | 1 != 'a'
| ^------^
|
= Cannot compare expressions of type `Integer` and `String`
");
insta::assert_snapshot!(env.parse_err(r#"'a' == true"#), @"
--> 1:1
|
1 | 'a' == true
| ^---------^
|
= Cannot compare expressions of type `String` and `Boolean`
");
insta::assert_snapshot!(env.parse_err(r#"'a' != 1"#), @"
--> 1:1
|
1 | 'a' != 1
| ^------^
|
= Cannot compare expressions of type `String` and `Integer`
");
insta::assert_snapshot!(env.parse_err(r#"'a' == label("", "")"#), @r#"
--> 1:1
|
1 | 'a' == label("", "")
| ^------------------^
|
= Cannot compare expressions of type `String` and `Template`
"#);
insta::assert_snapshot!(env.parse_err(r#"'a' > 1"#), @"
--> 1:1
|
1 | 'a' > 1
| ^-----^
|
= Cannot compare expressions of type `String` and `Integer`
");
insta::assert_snapshot!(env.parse_err(r#"description.first_line().foo()"#), @"
--> 1:26
|
1 | description.first_line().foo()
| ^-^
|
= Method `foo` doesn't exist for type `String`
");
insta::assert_snapshot!(env.parse_err(r#"10000000000000000000"#), @"
--> 1:1
|
1 | 10000000000000000000
| ^------------------^
|
= Invalid integer literal
number too large to fit in target type
");
insta::assert_snapshot!(env.parse_err(r#"42.foo()"#), @"
--> 1:4
|
1 | 42.foo()
| ^-^
|
= Method `foo` doesn't exist for type `Integer`
");
insta::assert_snapshot!(env.parse_err(r#"(-empty)"#), @"
--> 1:3
|
1 | (-empty)
| ^---^
|
= Expected expression of type `Integer`, but actual type is `Boolean`
");
insta::assert_snapshot!(env.parse_err(r#"("foo" ++ "bar").baz()"#), @r#"
--> 1:18
|
1 | ("foo" ++ "bar").baz()
| ^-^
|
= Method `baz` doesn't exist for type `Template`
"#);
insta::assert_snapshot!(env.parse_err(r#"description.contains()"#), @"
--> 1:22
|
1 | description.contains()
| ^
|
= Function `contains`: Expected 1 arguments
");
insta::assert_snapshot!(env.parse_err(r#"description.first_line("foo")"#), @r#"
--> 1:24
|
1 | description.first_line("foo")
| ^---^
|
= Function `first_line`: Expected 0 arguments
"#);
insta::assert_snapshot!(env.parse_err(r#"label()"#), @"
--> 1:7
|
1 | label()
| ^
|
= Function `label`: Expected 2 arguments
");
insta::assert_snapshot!(env.parse_err(r#"label("foo", "bar", "baz")"#), @r#"
--> 1:7
|
1 | label("foo", "bar", "baz")
| ^-----------------^
|
= Function `label`: Expected 2 arguments
"#);
insta::assert_snapshot!(env.parse_err(r#"if()"#), @"
--> 1:4
|
1 | if()
| ^
|
= Function `if`: Expected 2 to 3 arguments
");
insta::assert_snapshot!(env.parse_err(r#"if("foo", "bar", "baz", "quux")"#), @r#"
--> 1:4
|
1 | if("foo", "bar", "baz", "quux")
| ^-------------------------^
|
= Function `if`: Expected 2 to 3 arguments
"#);
insta::assert_snapshot!(env.parse_err(r#"pad_start("foo", fill_char = "bar", "baz")"#), @r#"
--> 1:37
|
1 | pad_start("foo", fill_char = "bar", "baz")
| ^---^
|
= Function `pad_start`: Positional argument follows keyword argument
"#);
insta::assert_snapshot!(env.parse_err(r#"if(label("foo", "bar"), "baz")"#), @r#"
--> 1:4
|
1 | if(label("foo", "bar"), "baz")
| ^-----------------^
|
= Expected expression of type `Boolean`, but actual type is `Template`
"#);
insta::assert_snapshot!(env.parse_err(r#"|x| description"#), @"
--> 1:1
|
1 | |x| description
| ^-------------^
|
= Lambda cannot be defined here
");
}
#[test]
fn test_self_keyword() {
let mut env = TestTemplateEnv::new();
env.add_keyword("say_hello", || literal("Hello".to_owned()));
insta::assert_snapshot!(env.render_ok(r#"self.say_hello()"#), @"Hello");
insta::assert_snapshot!(env.parse_err(r#"self"#), @"
--> 1:1
|
1 | self
| ^--^
|
= Expected expression of type `Template`, but actual type is `Self`
");
}
#[test]
fn test_boolean_cast() {
let mut env = TestTemplateEnv::new();
env.add_keyword("empty_bstr", || literal(BString::from("")));
env.add_keyword("nonempty_bstr", || literal(BString::from("a")));
insta::assert_snapshot!(env.render_ok("if(empty_bstr, true, false)"), @"false");
insta::assert_snapshot!(env.render_ok("if(nonempty_bstr, true, false)"), @"true");
env.add_keyword("empty_bstr_list", || literal::<Vec<BString>>(vec![]));
env.add_keyword("nonempty_bstr_list", || literal(vec![BString::from("")]));
insta::assert_snapshot!(env.render_ok("if(empty_bstr_list, true, false)"), @"false");
insta::assert_snapshot!(env.render_ok("if(nonempty_bstr_list, true, false)"), @"true");
insta::assert_snapshot!(env.render_ok(r#"if("", true, false)"#), @"false");
insta::assert_snapshot!(env.render_ok(r#"if("a", true, false)"#), @"true");
env.add_keyword("sl0", || literal::<Vec<String>>(vec![]));
env.add_keyword("sl1", || literal(vec!["".to_owned()]));
insta::assert_snapshot!(env.render_ok(r#"if(sl0, true, false)"#), @"false");
insta::assert_snapshot!(env.render_ok(r#"if(sl1, true, false)"#), @"true");
// No implicit cast of integer
insta::assert_snapshot!(env.parse_err(r#"if(0, true, false)"#), @"
--> 1:4
|
1 | if(0, true, false)
| ^
|
= Expected expression of type `Boolean`, but actual type is `Integer`
");
// Optional integer can be converted to boolean, and Some(0) is truthy.
env.add_keyword("none_i64", || literal(None::<i64>));
env.add_keyword("some_i64", || literal(Some(0)));
insta::assert_snapshot!(env.render_ok(r#"if(none_i64, true, false)"#), @"false");
insta::assert_snapshot!(env.render_ok(r#"if(some_i64, true, false)"#), @"true");
// Property errors do not evaluate
insta::assert_snapshot!(
env.render_ok("if(-none_i64 == 1, true, false)"),
@"<Error: No Integer available>"
);
insta::assert_snapshot!(env.parse_err(r#"if(label("", ""), true, false)"#), @r#"
--> 1:4
|
1 | if(label("", ""), true, false)
| ^-----------^
|
= Expected expression of type `Boolean`, but actual type is `Template`
"#);
insta::assert_snapshot!(env.parse_err(r#"if(sl0.map(|x| x), true, false)"#), @"
--> 1:4
|
1 | if(sl0.map(|x| x), true, false)
| ^------------^
|
= Expected expression of type `Boolean`, but actual type is `AnyList`
");
env.add_keyword("empty_email", || literal(Email("".to_owned())));
env.add_keyword("nonempty_email", || {
literal(Email("local@domain".to_owned()))
});
insta::assert_snapshot!(env.render_ok(r#"if(empty_email, true, false)"#), @"false");
insta::assert_snapshot!(env.render_ok(r#"if(nonempty_email, true, false)"#), @"true");
// even boolean config values must be extracted
env.add_keyword("config_bool", || literal(ConfigValue::from(true)));
insta::assert_snapshot!(env.parse_err("if(config_bool, true, false)"), @"
--> 1:4
|
1 | if(config_bool, true, false)
| ^---------^
|
= Expected expression of type `Boolean`, but actual type is `ConfigValue`
");
// misc uncastable types
env.add_keyword("signature", || {
literal(new_signature("Test User", "test.user@example.com"))
});
env.add_keyword("size_hint", || literal((5, None)));
env.add_keyword("timestamp", || literal(new_timestamp(0, 0)));
env.add_keyword("timestamp_range", || {
literal(TimestampRange {
start: new_timestamp(0, 0),
end: new_timestamp(0, 0),
})
});
assert_matches!(
env.parse_err_kind("if(signature, true, false)"),
TemplateParseErrorKind::Expression(_)
);
assert_matches!(
env.parse_err_kind("if(size_hint, true, false)"),
TemplateParseErrorKind::Expression(_)
);
assert_matches!(
env.parse_err_kind("if(timestamp, true, false)"),
TemplateParseErrorKind::Expression(_)
);
assert_matches!(
env.parse_err_kind("if(timestamp_range, true, false)"),
TemplateParseErrorKind::Expression(_)
);
assert_matches!(
env.parse_err_kind("if(if(true, true), true, false)"),
TemplateParseErrorKind::Expression(_)
);
assert_matches!(
env.parse_err_kind("if(sl0.map(|s| s), true, false)"),
TemplateParseErrorKind::Expression(_)
);
}
#[test]
fn test_arithmetic_operation() {
let mut env = TestTemplateEnv::new();
env.add_keyword("none_i64", || literal(None::<i64>));
env.add_keyword("some_i64", || literal(Some(1)));
env.add_keyword("i64_min", || literal(i64::MIN));
env.add_keyword("i64_max", || literal(i64::MAX));
insta::assert_snapshot!(env.render_ok(r#"-1"#), @"-1");
insta::assert_snapshot!(env.render_ok(r#"--2"#), @"2");
insta::assert_snapshot!(env.render_ok(r#"-(3)"#), @"-3");
insta::assert_snapshot!(env.render_ok(r#"1 + 2"#), @"3");
insta::assert_snapshot!(env.render_ok(r#"2 * 3"#), @"6");
insta::assert_snapshot!(env.render_ok(r#"1 + 2 * 3"#), @"7");
insta::assert_snapshot!(env.render_ok(r#"4 / 2"#), @"2");
insta::assert_snapshot!(env.render_ok(r#"5 / 2"#), @"2");
insta::assert_snapshot!(env.render_ok(r#"5 % 2"#), @"1");
// Since methods of the contained value can be invoked, it makes sense
// to apply operators to optional integers as well.
insta::assert_snapshot!(env.render_ok(r#"-none_i64"#), @"<Error: No Integer available>");
insta::assert_snapshot!(env.render_ok(r#"-some_i64"#), @"-1");
insta::assert_snapshot!(env.render_ok(r#"some_i64 + some_i64"#), @"2");
insta::assert_snapshot!(env.render_ok(r#"some_i64 + none_i64"#), @"<Error: No Integer available>");
insta::assert_snapshot!(env.render_ok(r#"none_i64 + some_i64"#), @"<Error: No Integer available>");
insta::assert_snapshot!(env.render_ok(r#"none_i64 + none_i64"#), @"<Error: No Integer available>");
// No panic on integer overflow.
insta::assert_snapshot!(
env.render_ok(r#"-i64_min"#),
@"<Error: Attempt to negate with overflow>");
insta::assert_snapshot!(
env.render_ok(r#"i64_max + 1"#),
@"<Error: Attempt to add with overflow>");
insta::assert_snapshot!(
env.render_ok(r#"i64_min - 1"#),
@"<Error: Attempt to subtract with overflow>");
insta::assert_snapshot!(
env.render_ok(r#"i64_max * 2"#),
@"<Error: Attempt to multiply with overflow>");
insta::assert_snapshot!(
env.render_ok(r#"i64_min / -1"#),
@"<Error: Attempt to divide with overflow>");
insta::assert_snapshot!(
env.render_ok(r#"1 / 0"#),
@"<Error: Attempt to divide by zero>");
insta::assert_snapshot!(
env.render_ok("i64_min % -1"),
@"<Error: Attempt to divide with overflow>");
insta::assert_snapshot!(
env.render_ok(r#"1 % 0"#),
@"<Error: Attempt to divide by zero>");
}
#[test]
fn test_relational_operation() {
let mut env = TestTemplateEnv::new();
env.add_keyword("none_i64", || literal(None::<i64>));
env.add_keyword("some_i64_0", || literal(Some(0_i64)));
env.add_keyword("some_i64_1", || literal(Some(1_i64)));
insta::assert_snapshot!(env.render_ok(r#"1 >= 1"#), @"true");
insta::assert_snapshot!(env.render_ok(r#"0 >= 1"#), @"false");
insta::assert_snapshot!(env.render_ok(r#"2 > 1"#), @"true");
insta::assert_snapshot!(env.render_ok(r#"1 > 1"#), @"false");
insta::assert_snapshot!(env.render_ok(r#"1 <= 1"#), @"true");
insta::assert_snapshot!(env.render_ok(r#"2 <= 1"#), @"false");
insta::assert_snapshot!(env.render_ok(r#"0 < 1"#), @"true");
insta::assert_snapshot!(env.render_ok(r#"1 < 1"#), @"false");
// none < some
insta::assert_snapshot!(env.render_ok(r#"none_i64 < some_i64_0"#), @"true");
insta::assert_snapshot!(env.render_ok(r#"some_i64_0 > some_i64_1"#), @"false");
insta::assert_snapshot!(env.render_ok(r#"none_i64 < 0"#), @"true");
insta::assert_snapshot!(env.render_ok(r#"1 > some_i64_0"#), @"true");
// invalid comparisons
assert_matches!(
env.parse_err_kind("42 >= true"),
TemplateParseErrorKind::Expression(_)
);
assert_matches!(
env.parse_err_kind("none_i64 >= true"),
TemplateParseErrorKind::Expression(_)
);
// un-comparable types
env.add_keyword("str_list", || {
literal(vec!["foo".to_owned(), "bar".to_owned()])
});
env.add_keyword("cfg_val", || {
literal(ConfigValue::from_iter([("foo", "bar")]))
});
env.add_keyword("some_cfg", || literal(Some(ConfigValue::from(1))));
env.add_keyword("signature", || {
literal(new_signature("User", "user@example.com"))
});
env.add_keyword("email", || literal(Email("me@example.com".to_owned())));
env.add_keyword("size_hint", || literal((10, None)));
env.add_keyword("timestamp", || literal(new_timestamp(0, 0)));
env.add_keyword("timestamp_range", || {
literal(TimestampRange {
start: new_timestamp(0, 0),
end: new_timestamp(0, 0),
})
});
assert_matches!(
env.parse_err_kind("'a' >= 'a'"),
TemplateParseErrorKind::Expression(_)
);
assert_matches!(
env.parse_err_kind("str_list >= str_list"),
TemplateParseErrorKind::Expression(_)
);
assert_matches!(
env.parse_err_kind("true >= true"),
TemplateParseErrorKind::Expression(_)
);
assert_matches!(
env.parse_err_kind("cfg_val >= cfg_val"),
TemplateParseErrorKind::Expression(_)
);
assert_matches!(
env.parse_err_kind("some_cfg >= some_cfg"),
TemplateParseErrorKind::Expression(_)
);
assert_matches!(
env.parse_err_kind("signature >= signature"),
TemplateParseErrorKind::Expression(_)
);
assert_matches!(
env.parse_err_kind("email >= email"),
TemplateParseErrorKind::Expression(_)
);
assert_matches!(
env.parse_err_kind("size_hint >= size_hint"),
TemplateParseErrorKind::Expression(_)
);
assert_matches!(
env.parse_err_kind("timestamp >= timestamp"),
TemplateParseErrorKind::Expression(_)
);
assert_matches!(
env.parse_err_kind("timestamp_range >= timestamp_range"),
TemplateParseErrorKind::Expression(_)
);
assert_matches!(
env.parse_err_kind("label('', '') >= label('', '')"),
TemplateParseErrorKind::Expression(_)
);
assert_matches!(
env.parse_err_kind("if(true, true) >= if(true, true)"),
TemplateParseErrorKind::Expression(_)
);
assert_matches!(
env.parse_err_kind("str_list.map(|s| s) >= str_list.map(|s| s)"),
TemplateParseErrorKind::Expression(_)
);
}
#[test]
fn test_logical_operation() {
let mut env = TestTemplateEnv::new();
env.add_keyword("none_i64", || literal::<Option<i64>>(None));
env.add_keyword("some_i64_0", || literal(Some(0_i64)));
env.add_keyword("some_i64_1", || literal(Some(1_i64)));
env.add_keyword("bstr1", || literal(BString::from("1")));
env.add_keyword("bstr2", || literal(BString::from("2")));
env.add_keyword("email1", || literal(Email("local-1@domain".to_owned())));
env.add_keyword("email2", || literal(Email("local-2@domain".to_owned())));
insta::assert_snapshot!(env.render_ok(r#"!false"#), @"true");
insta::assert_snapshot!(env.render_ok(r#"false || !false"#), @"true");
insta::assert_snapshot!(env.render_ok(r#"false && true"#), @"false");
insta::assert_snapshot!(env.render_ok(r#"true == true"#), @"true");
insta::assert_snapshot!(env.render_ok(r#"true == false"#), @"false");
insta::assert_snapshot!(env.render_ok(r#"true != true"#), @"false");
insta::assert_snapshot!(env.render_ok(r#"true != false"#), @"true");
insta::assert_snapshot!(env.render_ok(r#"1 == 1"#), @"true");
insta::assert_snapshot!(env.render_ok(r#"1 == 2"#), @"false");
insta::assert_snapshot!(env.render_ok(r#"1 != 1"#), @"false");
insta::assert_snapshot!(env.render_ok(r#"1 != 2"#), @"true");
insta::assert_snapshot!(env.render_ok(r#"none_i64 == none_i64"#), @"true");
insta::assert_snapshot!(env.render_ok(r#"some_i64_0 != some_i64_0"#), @"false");
insta::assert_snapshot!(env.render_ok(r#"none_i64 == 0"#), @"false");
insta::assert_snapshot!(env.render_ok(r#"some_i64_0 != 0"#), @"false");
insta::assert_snapshot!(env.render_ok(r#"1 == some_i64_1"#), @"true");
insta::assert_snapshot!(env.render_ok("bstr1 == bstr1"), @"true");
insta::assert_snapshot!(env.render_ok("bstr1 == bstr2"), @"false");
insta::assert_snapshot!(env.render_ok("bstr1 == '1'"), @"true");
insta::assert_snapshot!(env.render_ok("'2' != bstr2"), @"false");
insta::assert_snapshot!(env.render_ok(r#"'a' == 'a'"#), @"true");
insta::assert_snapshot!(env.render_ok(r#"'a' == 'b'"#), @"false");
insta::assert_snapshot!(env.render_ok(r#"'a' != 'a'"#), @"false");
insta::assert_snapshot!(env.render_ok(r#"'a' != 'b'"#), @"true");
insta::assert_snapshot!(env.render_ok(r#"email1 == email1"#), @"true");
insta::assert_snapshot!(env.render_ok(r#"email1 == email2"#), @"false");
insta::assert_snapshot!(env.render_ok(r#"email1 == 'local-1@domain'"#), @"true");
insta::assert_snapshot!(env.render_ok(r#"email1 != 'local-2@domain'"#), @"true");
insta::assert_snapshot!(env.render_ok(r#"'local-1@domain' == email1"#), @"true");
insta::assert_snapshot!(env.render_ok(r#"'local-2@domain' != email1"#), @"true");
insta::assert_snapshot!(env.render_ok(r#" !"" "#), @"true");
insta::assert_snapshot!(env.render_ok(r#" "" || "a".lines() "#), @"true");
// Short-circuiting
env.add_keyword("bad_bool", || new_error_property::<bool>("Bad"));
insta::assert_snapshot!(env.render_ok(r#"false && bad_bool"#), @"false");
insta::assert_snapshot!(env.render_ok(r#"true && bad_bool"#), @"<Error: Bad>");
insta::assert_snapshot!(env.render_ok(r#"false || bad_bool"#), @"<Error: Bad>");
insta::assert_snapshot!(env.render_ok(r#"true || bad_bool"#), @"true");
// Invalid comparisons
assert_matches!(
env.parse_err_kind("some_i64_0 == '0'"),
TemplateParseErrorKind::Expression(_)
);
assert_matches!(
env.parse_err_kind("email1 == 42"),
TemplateParseErrorKind::Expression(_)
);
// Un-comparable types
env.add_keyword("str_list", || {
literal(vec!["foo".to_owned(), "bar".to_owned()])
});
env.add_keyword("cfg_val", || {
literal(ConfigValue::from_iter([("foo", "bar")]))
});
env.add_keyword("some_cfg", || literal(Some(ConfigValue::from(true))));
env.add_keyword("signature", || {
literal(new_signature("User", "user@example.com"))
});
env.add_keyword("size_hint", || literal((10, None)));
env.add_keyword("timestamp", || literal(new_timestamp(0, 0)));
env.add_keyword("timestamp_range", || {
literal(TimestampRange {
start: new_timestamp(0, 0),
end: new_timestamp(0, 0),
})
});
assert_matches!(
env.parse_err_kind("str_list == str_list"),
TemplateParseErrorKind::Expression(_)
);
assert_matches!(
env.parse_err_kind("cfg_val == cfg_val"),
TemplateParseErrorKind::Expression(_)
);
assert_matches!(
env.parse_err_kind("some_cfg == some_cfg"),
TemplateParseErrorKind::Expression(_)
);
assert_matches!(
env.parse_err_kind("signature == signature"),
TemplateParseErrorKind::Expression(_)
);
assert_matches!(
env.parse_err_kind("size_hint == size_hint"),
TemplateParseErrorKind::Expression(_)
);
assert_matches!(
env.parse_err_kind("timestamp == timestamp"),
TemplateParseErrorKind::Expression(_)
);
assert_matches!(
env.parse_err_kind("timestamp_range == timestamp_range"),
TemplateParseErrorKind::Expression(_)
);
assert_matches!(
env.parse_err_kind("label('', '') == label('', '')"),
TemplateParseErrorKind::Expression(_)
);
assert_matches!(
env.parse_err_kind("if(true, true) == if(true, true)"),
TemplateParseErrorKind::Expression(_)
);
assert_matches!(
env.parse_err_kind("str_list.map(|s| s) == str_list.map(|s| s)"),
TemplateParseErrorKind::Expression(_)
);
}
#[test]
fn test_list_method() {
let mut env = TestTemplateEnv::new();
env.add_keyword("empty", || literal(true));
env.add_keyword("sep", || literal("sep".to_owned()));
insta::assert_snapshot!(env.render_ok(r#""".lines().len()"#), @"0");
insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().len()"#), @"3");
insta::assert_snapshot!(env.render_ok(r#""".lines().join("|")"#), @"");
insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().join("|")"#), @"a|b|c");
// Null separator
insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().join("\0")"#), @"a\0b\0c");
// Keyword as separator
insta::assert_snapshot!(
env.render_ok(r#""a\nb\nc".lines().join(sep.upper())"#),
@"aSEPbSEPc");
insta::assert_snapshot!(
env.render_ok(r#""a\nbb\nc".lines().filter(|s| s.len() == 1)"#),
@"a c");
insta::assert_snapshot!(
env.render_ok(r#""a\nb\nc".lines().map(|s| s ++ s)"#),
@"aa bb cc");
// Test any() method
insta::assert_snapshot!(
env.render_ok(r#""a\nb\nc".lines().any(|s| s == "b")"#),
@"true");
insta::assert_snapshot!(
env.render_ok(r#""a\nb\nc".lines().any(|s| s == "d")"#),
@"false");
insta::assert_snapshot!(
env.render_ok(r#""".lines().any(|s| s == "a")"#),
@"false");
// any() with more complex predicate
insta::assert_snapshot!(
env.render_ok(r#""ax\nbb\nc".lines().any(|s| s.contains("x"))"#),
@"true");
insta::assert_snapshot!(
env.render_ok(r#""a\nbb\nc".lines().any(|s| s.len() > 1)"#),
@"true");
// Test all() method
insta::assert_snapshot!(
env.render_ok(r#""a\nb\nc".lines().all(|s| s.len() == 1)"#),
@"true");
insta::assert_snapshot!(
env.render_ok(r#""a\nbb\nc".lines().all(|s| s.len() == 1)"#),
@"false");
// Empty list returns true for all()
insta::assert_snapshot!(
env.render_ok(r#""".lines().all(|s| s == "a")"#),
@"true");
// all() with more complex predicate
insta::assert_snapshot!(
env.render_ok(r#""ax\nbx\ncx".lines().all(|s| s.ends_with("x"))"#),
@"true");
insta::assert_snapshot!(
env.render_ok(r#""a\nbb\nc".lines().all(|s| s.len() < 3)"#),
@"true");
// Combining any/all with filter
insta::assert_snapshot!(
env.render_ok(r#""a\nbb\nccc".lines().filter(|s| s.len() > 1).any(|s| s == "bb")"#),
@"true");
insta::assert_snapshot!(
env.render_ok(r#""a\nbb\nccc".lines().filter(|s| s.len() > 1).all(|s| s.len() >= 2)"#),
@"true");
// Nested any/all operations
insta::assert_snapshot!(
env.render_ok(r#"if("a\nb".lines().any(|s| s == "a"), "found", "not found")"#),
@"found");
insta::assert_snapshot!(
env.render_ok(r#"if("a\nb".lines().all(|s| s.len() == 1), "all single", "not all")"#),
@"all single");
// Global keyword in item template
insta::assert_snapshot!(
env.render_ok(r#""a\nb\nc".lines().map(|s| s ++ empty)"#),
@"atrue btrue ctrue");
// Global keyword in item template shadowing 'self'
insta::assert_snapshot!(
env.render_ok(r#""a\nb\nc".lines().map(|self| self ++ empty)"#),
@"atrue btrue ctrue");
// Override global keyword 'empty'
insta::assert_snapshot!(
env.render_ok(r#""a\nb\nc".lines().map(|empty| empty)"#),
@"a b c");
// Nested map operations
insta::assert_snapshot!(
env.render_ok(r#""a\nb\nc".lines().map(|s| "x\ny".lines().map(|t| s ++ t))"#),
@"ax ay bx by cx cy");
// Nested map/join operations
insta::assert_snapshot!(
env.render_ok(r#""a\nb\nc".lines().map(|s| "x\ny".lines().map(|t| s ++ t).join(",")).join(";")"#),
@"ax,ay;bx,by;cx,cy");
// Nested string operations
insta::assert_snapshot!(
env.render_ok(r#""! a\n!b\nc\n end".remove_suffix("end").trim_end().lines().map(|s| s.remove_prefix("!").trim_start())"#),
@"a b c");
// Lambda expression in alias
env.add_alias("identity", "|x| x");
insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().map(identity)"#), @"a b c");
// Not a lambda expression
insta::assert_snapshot!(env.parse_err(r#""a".lines().map(empty)"#), @r#"
--> 1:17
|
1 | "a".lines().map(empty)
| ^---^
|
= Expected lambda expression
"#);
// Bad lambda parameter count
insta::assert_snapshot!(env.parse_err(r#""a".lines().map(|| "")"#), @r#"
--> 1:18
|
1 | "a".lines().map(|| "")
| ^
|
= Expected 1 lambda parameters
"#);
insta::assert_snapshot!(env.parse_err(r#""a".lines().map(|a, b| "")"#), @r#"
--> 1:18
|
1 | "a".lines().map(|a, b| "")
| ^--^
|
= Expected 1 lambda parameters
"#);
// Bad lambda output
insta::assert_snapshot!(env.parse_err(r#""a".lines().filter(|s| s ++ "\n")"#), @r#"
--> 1:24
|
1 | "a".lines().filter(|s| s ++ "\n")
| ^-------^
|
= Expected expression of type `Boolean`, but actual type is `Template`
"#);
// Error in any() and all()
insta::assert_snapshot!(env.parse_err(r#""a".lines().any(|s| s.len())"#), @r#"
--> 1:21
|
1 | "a".lines().any(|s| s.len())
| ^-----^
|
= Expected expression of type `Boolean`, but actual type is `Integer`
"#);
// Bad lambda output for all()
insta::assert_snapshot!(env.parse_err(r#""a".lines().all(|s| s ++ "x")"#), @r#"
--> 1:21
|
1 | "a".lines().all(|s| s ++ "x")
| ^------^
|
= Expected expression of type `Boolean`, but actual type is `Template`
"#);
// Wrong parameter count for any()
insta::assert_snapshot!(env.parse_err(r#""a".lines().any(|| true)"#), @r#"
--> 1:18
|
1 | "a".lines().any(|| true)
| ^
|
= Expected 1 lambda parameters
"#);
// Wrong parameter count for all()
insta::assert_snapshot!(env.parse_err(r#""a".lines().all(|a, b| true)"#), @r#"
--> 1:18
|
1 | "a".lines().all(|a, b| true)
| ^--^
|
= Expected 1 lambda parameters
"#);
// Error in lambda expression
insta::assert_snapshot!(env.parse_err(r#""a".lines().map(|s| s.unknown())"#), @r#"
--> 1:23
|
1 | "a".lines().map(|s| s.unknown())
| ^-----^
|
= Method `unknown` doesn't exist for type `String`
"#);
// Error in lambda alias
env.add_alias("too_many_params", "|x, y| x");
insta::assert_snapshot!(env.parse_err(r#""a".lines().map(too_many_params)"#), @r#"
--> 1:17
|
1 | "a".lines().map(too_many_params)
| ^-------------^
|
= In alias `too_many_params`
--> 1:2
|
1 | |x, y| x
| ^--^
|
= Expected 1 lambda parameters
"#);
// List.first()
insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().first()"#), @"a");
insta::assert_snapshot!(env.render_ok(r#""".lines().first()"#), @"<Error: List is empty>");
// List.last()
insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().last()"#), @"c");
insta::assert_snapshot!(env.render_ok(r#""".lines().last()"#), @"<Error: List is empty>");
// List.get(index)
insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().get(0)"#), @"a");
insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().get(1)"#), @"b");
insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().get(2)"#), @"c");
insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().get(3)"#), @"<Error: Index 3 out of bounds>");
insta::assert_snapshot!(env.render_ok(r#""".lines().get(0)"#), @"<Error: Index 0 out of bounds>");
// List.reverse()
insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().reverse().join("|")"#), @"c|b|a");
insta::assert_snapshot!(env.render_ok(r#""".lines().reverse().join("|")"#), @"");
// List.skip(count)
insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().skip(0).join("|")"#), @"a|b|c");
insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().skip(1).join("|")"#), @"b|c");
insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().skip(2).join("|")"#), @"c");
insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().skip(3).join("|")"#), @"");
insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().skip(10).join("|")"#), @"");
// List.take(count)
insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().take(0).join("|")"#), @"");
insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().take(1).join("|")"#), @"a");
insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().take(2).join("|")"#), @"a|b");
insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().take(3).join("|")"#), @"a|b|c");
insta::assert_snapshot!(env.render_ok(r#""a\nb\nc".lines().take(10).join("|")"#), @"a|b|c");
// Combining skip and take
insta::assert_snapshot!(env.render_ok(r#""a\nb\nc\nd".lines().skip(1).take(2).join("|")"#), @"b|c");
}
#[test]
fn test_byte_string_method() {
let mut env = TestTemplateEnv::new();
env.add_keyword("empty", || literal(BString::from("")));
env.add_keyword("foo", || literal(BString::from("foo")));
env.add_keyword("bar", || literal(BString::from("bar")));
env.add_keyword("foobar", || literal(BString::from("foobar")));
env.add_keyword("foo_ws", || literal(BString::from(" \n \r foo \t \r ")));
env.add_keyword("foo_bar_nl", || literal(BString::from("foo\nbar\n")));
env.add_keyword("foo_bar_case", || literal(BString::from("foo BAR")));
env.add_keyword("odd", || literal(BString::from(b"\x80")));
env.add_keyword("odd_ws", || literal(BString::from(b" \x80 ")));
env.add_keyword("odd_case", || literal(BString::from(b"A\x80z")));
insta::assert_snapshot!(env.render_ok("empty.len()"), @"0");
insta::assert_snapshot!(env.render_ok("foo.len()"), @"3");
insta::assert_snapshot!(env.render_ok("odd.len()"), @"1");
insta::assert_snapshot!(env.render_ok("foobar.contains(foo)"), @"true");
insta::assert_snapshot!(env.render_ok("foo.contains(foobar)"), @"false");
insta::assert_snapshot!(env.render_ok("foo.contains('foo')"), @"true");
insta::assert_snapshot!(env.render_ok("odd_case.contains(odd)"), @"true");
insta::assert_snapshot!(env.render_ok("foobar.match(regex:'[a-f]o+')"), @"foo");
insta::assert_snapshot!(env.render_ok("foobar.match(regex:'^$')"), @"");
insta::assert_snapshot!(env.render_ok("json(odd.match(regex:'(?-u:.)'))"), @"[128]");
insta::assert_snapshot!(env.render_ok("foobar.starts_with(foo)"), @"true");
insta::assert_snapshot!(env.render_ok("foobar.starts_with(bar)"), @"false");
insta::assert_snapshot!(env.render_ok("foobar.starts_with('foo')"), @"true");
insta::assert_snapshot!(env.render_ok("foobar.ends_with(foo)"), @"false");
insta::assert_snapshot!(env.render_ok("foobar.ends_with(bar)"), @"true");
insta::assert_snapshot!(env.render_ok("foobar.ends_with('foo')"), @"false");
insta::assert_snapshot!(env.render_ok("odd_case.starts_with('A' ++ odd)"), @"true");
insta::assert_snapshot!(env.render_ok("odd_case.ends_with(odd ++ 'z')"), @"true");
insta::assert_snapshot!(env.render_ok("foobar.remove_prefix(foo)"), @"bar");
insta::assert_snapshot!(env.render_ok("foobar.remove_prefix(bar)"), @"foobar");
insta::assert_snapshot!(env.render_ok("foobar.remove_prefix('foo')"), @"bar");
insta::assert_snapshot!(env.render_ok("foobar.remove_suffix(foo)"), @"foobar");
insta::assert_snapshot!(env.render_ok("foobar.remove_suffix(bar)"), @"foo");
insta::assert_snapshot!(env.render_ok("foobar.remove_suffix('foo')"), @"foobar");
insta::assert_snapshot!(env.render_ok("json(odd_case.remove_prefix('A'))"), @"[128,122]");
insta::assert_snapshot!(env.render_ok("json(odd_case.remove_prefix('A' ++ odd))"), @"[122]");
insta::assert_snapshot!(env.render_ok("json(odd_case.remove_suffix('z'))"), @"[65,128]");
insta::assert_snapshot!(env.render_ok("json(odd_case.remove_suffix(odd ++ 'z'))"), @"[65]");
insta::assert_snapshot!(env.render_ok("'|' ++ foo_ws.trim() ++ '|'"), @"|foo|");
insta::assert_snapshot!(env.render_ok("'|' ++ foo_ws.trim_start() ++ '|'"), @"|foo \t \r |");
insta::assert_snapshot!(env.render_ok("'|' ++ foo_ws.trim_end() ++ '|'"), @"| \n \r foo|");
insta::assert_snapshot!(env.render_ok("json(odd_ws.trim())"), @"[128]");
insta::assert_snapshot!(env.render_ok("json(odd_ws.trim_start())"), @"[128,32]");
insta::assert_snapshot!(env.render_ok("json(odd_ws.trim_end())"), @"[32,128]");
insta::assert_snapshot!(env.render_ok("bar.substr(0)"), @"bar");
insta::assert_snapshot!(env.render_ok("bar.substr(1)"), @"ar");
insta::assert_snapshot!(env.render_ok("bar.substr(2)"), @"r");
insta::assert_snapshot!(env.render_ok("bar.substr(3)"), @"");
insta::assert_snapshot!(env.render_ok("bar.substr(4)"), @"");
insta::assert_snapshot!(env.render_ok("bar.substr(-1)"), @"r");
insta::assert_snapshot!(env.render_ok("bar.substr(-2)"), @"ar");
insta::assert_snapshot!(env.render_ok("bar.substr(-3)"), @"bar");
insta::assert_snapshot!(env.render_ok("bar.substr(-4)"), @"bar");
insta::assert_snapshot!(env.render_ok("bar.substr(1, 0)"), @"");
insta::assert_snapshot!(env.render_ok("bar.substr(1, 2)"), @"a");
insta::assert_snapshot!(env.render_ok("bar.substr(1, -1)"), @"a");
insta::assert_snapshot!(env.render_ok("bar.substr(2, -1)"), @"");
insta::assert_snapshot!(env.render_ok("bar.substr(4, -4)"), @"");
insta::assert_snapshot!(env.render_ok("json(odd_case.substr(1, 2))"), @"[128]");
insta::assert_snapshot!(env.render_ok("'|' ++ empty.first_line() ++ '|'"), @"||");
insta::assert_snapshot!(env.render_ok("'|' ++ foo_bar_nl.first_line() ++ '|'"), @"|foo|");
insta::assert_snapshot!(env.render_ok("empty.lines().len()"), @"0");
insta::assert_snapshot!(env.render_ok("foo_bar_nl.lines()"), @"foo bar");
insta::assert_snapshot!(env.render_ok("json(odd.first_line())"), @"[128]");
insta::assert_snapshot!(env.render_ok("json(odd.lines())"), @"[[128]]");
insta::assert_snapshot!(env.render_ok("empty.split(' ').join(',')"), @"");
insta::assert_snapshot!(env.render_ok("foo_bar_case.split(' ').join(',')"), @"foo,BAR");
insta::assert_snapshot!(env.render_ok("foo_bar_case.split(' ', 0).join(',')"), @"");
insta::assert_snapshot!(env.render_ok("foo_bar_case.split(' ', 1).join(',')"), @"foo BAR");
insta::assert_snapshot!(env.render_ok("foo_bar_case.split(' ', 2).join(',')"), @"foo,BAR");
insta::assert_snapshot!(
env.render_ok("json(odd_case.split(regex:'(?-u)[^Az]'))"), @"[[65],[122]]");
insta::assert_snapshot!(env.render_ok("json(odd_case.split(regex:'A'))"), @"[[],[128,122]]");
insta::assert_snapshot!(env.render_ok("foo.replace('o', '*')"), @"f**");
insta::assert_snapshot!(env.render_ok("foo.replace('o', '*', 0)"), @"foo");
insta::assert_snapshot!(env.render_ok("foo.replace('o', '*', 1)"), @"f*o");
insta::assert_snapshot!(env.render_ok("foo.replace('o', '*', 2)"), @"f**");
insta::assert_snapshot!(env.render_ok("bar.replace(regex:'b(a)', '$0$1')"), @"baar");
insta::assert_snapshot!(env.render_ok("json(foo.replace('o', odd))"), @"[102,128,128]");
insta::assert_snapshot!(
env.render_ok("json(odd_case.replace(regex:'(?-u)[^Az]', ' '))"), @"[65,32,122]");
insta::assert_snapshot!(
env.render_ok("json(odd_case.replace(regex:'A', ' '))"), @"[32,128,122]");
insta::assert_snapshot!(env.render_ok("foo_bar_case.upper()"), @"FOO BAR");
insta::assert_snapshot!(env.render_ok("foo_bar_case.lower()"), @"foo bar");
insta::assert_snapshot!(env.render_ok("json(odd_case.upper())"), @"[65,128,90]");
insta::assert_snapshot!(env.render_ok("json(odd_case.lower())"), @"[97,128,122]");
}
#[test]
fn test_string_method() {
let mut env = TestTemplateEnv::new();
env.add_keyword("description", || literal("description 1".to_owned()));
env.add_keyword("bad_string", || new_error_property::<String>("Bad"));
insta::assert_snapshot!(env.render_ok(r#""".len()"#), @"0");
insta::assert_snapshot!(env.render_ok(r#""foo".len()"#), @"3");
insta::assert_snapshot!(env.render_ok(r#""💩".len()"#), @"4");
insta::assert_snapshot!(env.render_ok(r#""fooo".contains("foo")"#), @"true");
insta::assert_snapshot!(env.render_ok(r#""foo".contains("fooo")"#), @"false");
insta::assert_snapshot!(env.render_ok(r#"description.contains("description")"#), @"true");
insta::assert_snapshot!(
env.render_ok(r#""description 123".contains(description.first_line())"#),
@"true");
// String patterns are not stringifiable
insta::assert_snapshot!(env.parse_err(r#""fa".starts_with(regex:'[a-f]o+')"#), @r#"
--> 1:18
|
1 | "fa".starts_with(regex:'[a-f]o+')
| ^-------------^
|
= String patterns may not be used as expression values
"#);
// inner template error should propagate
insta::assert_snapshot!(env.render_ok(r#""foo".contains(bad_string)"#), @"<Error: Bad>");
insta::assert_snapshot!(
env.render_ok(r#""foo".contains("f" ++ bad_string) ++ "bar""#), @"<Error: Bad>bar");
insta::assert_snapshot!(
env.render_ok(r#""foo".contains(separate("o", "f", bad_string))"#), @"<Error: Bad>");
insta::assert_snapshot!(env.render_ok(r#""fooo".match(regex:'[a-f]o+')"#), @"fooo");
insta::assert_snapshot!(env.render_ok(r#""fa".match(regex:'[a-f]o+')"#), @"");
insta::assert_snapshot!(env.render_ok(r#""hello".match(regex:"h(ell)o")"#), @"hello");
insta::assert_snapshot!(env.render_ok(r#""HEllo".match(regex-i:"h(ell)o")"#), @"HEllo");
insta::assert_snapshot!(env.render_ok(r#""hEllo".match(glob:"h*o")"#), @"hEllo");
insta::assert_snapshot!(env.render_ok(r#""Hello".match(glob:"h*o")"#), @"");
insta::assert_snapshot!(env.render_ok(r#""HEllo".match(glob-i:"h*o")"#), @"HEllo");
insta::assert_snapshot!(env.render_ok(r#""hello".match("he")"#), @"he");
insta::assert_snapshot!(env.render_ok(r#""hello".match(substring:"he")"#), @"he");
insta::assert_snapshot!(env.render_ok(r#""hello".match(exact:"he")"#), @"");
// Evil regexes can cause invalid UTF-8 output, which nothing can
// really be done about given we're matching against non-UTF-8 stuff a
// lot as well.
insta::assert_snapshot!(env.render_ok(r#""🥺".match(regex:'(?-u)^(?:.)')"#), @"<Error: incomplete utf-8 byte sequence from index 0>");
insta::assert_snapshot!(env.parse_err(r#""hello".match(false)"#), @r#"
--> 1:15
|
1 | "hello".match(false)
| ^---^
|
= Expected string pattern
"#);
insta::assert_snapshot!(env.parse_err(r#""🥺".match(not-a-pattern:"abc")"#), @r#"
--> 1:11
|
1 | "🥺".match(not-a-pattern:"abc")
| ^-----------------^
|
= Bad string pattern
Invalid string pattern kind `not-a-pattern:`
"#);
insta::assert_snapshot!(env.render_ok(r#""".first_line()"#), @"");
insta::assert_snapshot!(env.render_ok(r#""foo\nbar".first_line()"#), @"foo");
insta::assert_snapshot!(env.render_ok(r#""".lines()"#), @"");
insta::assert_snapshot!(env.render_ok(r#""a\nb\nc\n".lines()"#), @"a b c");
insta::assert_snapshot!(env.render_ok(r#""".split(",")"#), @"");
insta::assert_snapshot!(env.render_ok(r#""a,b,c".split(",")"#), @"a b c");
insta::assert_snapshot!(env.render_ok(r#""a::b::c::d".split("::")"#), @"a b c d");
insta::assert_snapshot!(env.render_ok(r#""a,b,c,d".split(",", 0)"#), @"");
insta::assert_snapshot!(env.render_ok(r#""a,b,c,d".split(",", 2)"#), @"a b,c,d");
insta::assert_snapshot!(env.render_ok(r#""a,b,c,d".split(",", 3)"#), @"a b c,d");
insta::assert_snapshot!(env.render_ok(r#""a,b,c,d".split(",", 10)"#), @"a b c d");
insta::assert_snapshot!(env.render_ok(r#""abc".split(",", -1)"#), @"<Error: out of range integral type conversion attempted>");
insta::assert_snapshot!(env.render_ok(r#"json("a1b2c3".split(regex:'\d+'))"#), @r#"["a","b","c",""]"#);
insta::assert_snapshot!(env.render_ok(r#""foo bar baz".split(regex:'\s+')"#), @"foo bar baz");
insta::assert_snapshot!(env.render_ok(r#""a1b2c3d4".split(regex:'\d+', 3)"#), @"a b c3d4");
insta::assert_snapshot!(env.render_ok(r#"json("hello world".split(regex-i:"WORLD"))"#), @r#"["hello ",""]"#);
insta::assert_snapshot!(env.render_ok("''.upper()"), @"");
insta::assert_snapshot!(env.render_ok("'ABCabc 123!@#'.upper()"), @"ABCABC 123!@#");
insta::assert_snapshot!(env.render_ok("''.lower()"), @"");
insta::assert_snapshot!(env.render_ok("'ABCabc 123!@#'.lower()"), @"abcabc 123!@#");
insta::assert_snapshot!(env.render_ok(r#""".starts_with("")"#), @"true");
insta::assert_snapshot!(env.render_ok(r#""everything".starts_with("")"#), @"true");
insta::assert_snapshot!(env.render_ok(r#""".starts_with("foo")"#), @"false");
insta::assert_snapshot!(env.render_ok(r#""foo".starts_with("foo")"#), @"true");
insta::assert_snapshot!(env.render_ok(r#""foobar".starts_with("foo")"#), @"true");
insta::assert_snapshot!(env.render_ok(r#""foobar".starts_with("bar")"#), @"false");
insta::assert_snapshot!(env.render_ok(r#""".ends_with("")"#), @"true");
insta::assert_snapshot!(env.render_ok(r#""everything".ends_with("")"#), @"true");
insta::assert_snapshot!(env.render_ok(r#""".ends_with("foo")"#), @"false");
insta::assert_snapshot!(env.render_ok(r#""foo".ends_with("foo")"#), @"true");
insta::assert_snapshot!(env.render_ok(r#""foobar".ends_with("foo")"#), @"false");
insta::assert_snapshot!(env.render_ok(r#""foobar".ends_with("bar")"#), @"true");
insta::assert_snapshot!(env.render_ok(r#""".remove_prefix("wip: ")"#), @"");
insta::assert_snapshot!(
env.render_ok(r#""wip: testing".remove_prefix("wip: ")"#),
@"testing");
insta::assert_snapshot!(
env.render_ok(r#""bar@my.example.com".remove_suffix("@other.example.com")"#),
@"bar@my.example.com");
insta::assert_snapshot!(
env.render_ok(r#""bar@other.example.com".remove_suffix("@other.example.com")"#),
@"bar");
insta::assert_snapshot!(env.render_ok(r#"" \n \r \t \r ".trim()"#), @"");
insta::assert_snapshot!(env.render_ok(r#"" \n \r foo bar \t \r ".trim()"#), @"foo bar");
insta::assert_snapshot!(env.render_ok(r#"" \n \r \t \r ".trim_start()"#), @"");
insta::assert_snapshot!(env.render_ok(r#"" \n \r foo bar \t \r ".trim_start()"#), @"foo bar");
insta::assert_snapshot!(env.render_ok(r#"" \n \r \t \r ".trim_end()"#), @"");
insta::assert_snapshot!(env.render_ok(r#"" \n \r foo bar \t \r ".trim_end()"#), @"\n\r foo bar");
insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 0)"#), @"");
insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 1)"#), @"f");
insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 3)"#), @"foo");
insta::assert_snapshot!(env.render_ok(r#""foo".substr(0, 4)"#), @"foo");
insta::assert_snapshot!(env.render_ok(r#""foo".substr(1, 3)"#), @"oo");
insta::assert_snapshot!(env.render_ok(r#""foo".substr(1)"#), @"oo");
insta::assert_snapshot!(env.render_ok(r#""foo".substr(0)"#), @"foo");
insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(2, -1)"#), @"cde");
insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-3, 99)"#), @"def");
insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-3)"#), @"def");
insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-6, 99)"#), @"abcdef");
insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-7, 1)"#), @"a");
insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-100)"#), @"abcdef");
// non-ascii characters
insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(2, -1)"#), @"c💩");
insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, -3)"#), @"💩");
insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, -4)"#), @"");
insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(6, -3)"#), @"💩");
insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(7, -3)"#), @"");
insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, 4)"#), @"");
insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, 6)"#), @"");
insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3, 7)"#), @"💩");
insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-1, 7)"#), @"");
insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-3, 7)"#), @"");
insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-4, 7)"#), @"💩");
insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(0)"#), @"abc💩");
insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(1)"#), @"bc💩");
insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(3)"#), @"💩");
insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(4)"#), @"💩");
insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-5)"#), @"c💩");
insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-4)"#), @"💩");
insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-3)"#), @"");
insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-2)"#), @"");
insta::assert_snapshot!(env.render_ok(r#""abc💩".substr(-1)"#), @"");
// ranges with end > start are empty
insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(4, 2)"#), @"");
insta::assert_snapshot!(env.render_ok(r#""abcdef".substr(-2, -4)"#), @"");
insta::assert_snapshot!(env.render_ok(r#""hello".escape_json()"#), @r#""hello""#);
insta::assert_snapshot!(env.render_ok(r#""he \n ll \n \" o".escape_json()"#), @r#""he \n ll \n \" o""#);
// simple substring replacement
insta::assert_snapshot!(env.render_ok(r#""hello world".replace("world", "jj")"#), @"hello jj");
insta::assert_snapshot!(env.render_ok(r#""hello world world".replace("world", "jj")"#), @"hello jj jj");
insta::assert_snapshot!(env.render_ok(r#""hello".replace("missing", "jj")"#), @"hello");
// replace with limit >=0
insta::assert_snapshot!(env.render_ok(r#""hello world world".replace("world", "jj", 0)"#), @"hello world world");
insta::assert_snapshot!(env.render_ok(r#""hello world world".replace("world", "jj", 1)"#), @"hello jj world");
insta::assert_snapshot!(env.render_ok(r#""hello world world world".replace("world", "jj", 2)"#), @"hello jj jj world");
// replace with limit <0 (error due to negative limit)
insta::assert_snapshot!(env.render_ok(r#""hello world world".replace("world", "jj", -1)"#), @"<Error: out of range integral type conversion attempted>");
insta::assert_snapshot!(env.render_ok(r#""hello world world".replace("world", "jj", -5)"#), @"<Error: out of range integral type conversion attempted>");
// replace with regex patterns
insta::assert_snapshot!(env.render_ok(r#""hello123world456".replace(regex:'\d+', "X")"#), @"helloXworldX");
insta::assert_snapshot!(env.render_ok(r#""hello123world456".replace(regex:'\d+', "X", 1)"#), @"helloXworld456");
// replace with regex patterns (capture groups)
insta::assert_snapshot!(env.render_ok(r#""HELLO WORLD".replace(regex-i:"(hello) +(world)", "$2 $1")"#), @"WORLD HELLO");
insta::assert_snapshot!(env.render_ok(r#""abc123".replace(regex:"([a-z]+)([0-9]+)", "$2-$1")"#), @"123-abc");
insta::assert_snapshot!(env.render_ok(r#""foo123bar".replace(regex:'\d+', "[$0]")"#), @"foo[123]bar");
// replace with regex patterns (case insensitive)
insta::assert_snapshot!(env.render_ok(r#""Hello World".replace(regex-i:"hello", "hi")"#), @"hi World");
insta::assert_snapshot!(env.render_ok(r#""Hello World Hello".replace(regex-i:"hello", "hi")"#), @"hi World hi");
insta::assert_snapshot!(env.render_ok(r#""Hello World Hello".replace(regex-i:"hello", "hi", 1)"#), @"hi World Hello");
// replace with strings that look regex-y ($n patterns are always expanded)
insta::assert_snapshot!(env.render_ok(r#"'hello\d+world'.replace('\d+', "X")"#), @"helloXworld");
insta::assert_snapshot!(env.render_ok(r#""(foo)($1)bar".replace("$1", "$2")"#), @"(foo)()bar");
insta::assert_snapshot!(env.render_ok(r#""test(abc)end".replace("(abc)", "X")"#), @"testXend");
// replace with templates
insta::assert_snapshot!(env.render_ok(r#""hello world".replace("world", description.first_line())"#), @"hello description 1");
// replace with error
insta::assert_snapshot!(env.render_ok(r#""hello world".replace("world", bad_string)"#), @"<Error: Bad>");
}
#[test]
fn test_config_value_method() {
let mut env = TestTemplateEnv::new();
env.add_keyword("boolean", || literal(ConfigValue::from(true)));
env.add_keyword("integer", || literal(ConfigValue::from(42)));
env.add_keyword("string", || literal(ConfigValue::from("foo")));
env.add_keyword("string_list", || {
literal(ConfigValue::from_iter(["foo", "bar"]))
});
insta::assert_snapshot!(env.render_ok("boolean"), @"true");
insta::assert_snapshot!(env.render_ok("integer"), @"42");
insta::assert_snapshot!(env.render_ok("string"), @r#""foo""#);
insta::assert_snapshot!(env.render_ok("string_list"), @r#"["foo", "bar"]"#);
insta::assert_snapshot!(env.render_ok("boolean.as_boolean()"), @"true");
insta::assert_snapshot!(env.render_ok("integer.as_integer()"), @"42");
insta::assert_snapshot!(env.render_ok("string.as_string()"), @"foo");
insta::assert_snapshot!(env.render_ok("string_list.as_string_list()"), @"foo bar");
insta::assert_snapshot!(
env.render_ok("boolean.as_integer()"),
@"<Error: invalid type: boolean `true`, expected i64>");
insta::assert_snapshot!(
env.render_ok("integer.as_string()"),
@"<Error: invalid type: integer `42`, expected a string>");
insta::assert_snapshot!(
env.render_ok("string.as_string_list()"),
@r#"<Error: invalid type: string "foo", expected a sequence>"#);
insta::assert_snapshot!(
env.render_ok("string_list.as_boolean()"),
@"<Error: invalid type: sequence, expected a boolean>");
}
#[test]
fn test_signature_and_email_methods() {
let mut env = TestTemplateEnv::new();
env.add_keyword("author", || {
literal(new_signature("Test User", "test.user@example.com"))
});
insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user@example.com>");
insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Test User");
insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@example.com");
insta::assert_snapshot!(env.render_ok("author.email().local()"), @"test.user");
insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"example.com");
insta::assert_snapshot!(env.render_ok("author.timestamp()"), @"1970-01-01 00:00:00.000 +00:00");
env.add_keyword("author", || {
literal(new_signature("Another Test User", "test.user@example.com"))
});
insta::assert_snapshot!(env.render_ok(r#"author"#), @"Another Test User <test.user@example.com>");
insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Another Test User");
insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@example.com");
env.add_keyword("author", || {
literal(new_signature("Test User", "test.user@invalid@example.com"))
});
insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user@invalid@example.com>");
insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Test User");
insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@invalid@example.com");
insta::assert_snapshot!(env.render_ok("author.email().local()"), @"test.user");
insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"invalid@example.com");
env.add_keyword("author", || {
literal(new_signature("Test User", "test.user"))
});
insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user>");
insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user");
insta::assert_snapshot!(env.render_ok("author.email().local()"), @"test.user");
insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"");
env.add_keyword("author", || {
literal(new_signature("Test User", "test.user+tag@example.com"))
});
insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <test.user+tag@example.com>");
insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user+tag@example.com");
insta::assert_snapshot!(env.render_ok("author.email().local()"), @"test.user+tag");
insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"example.com");
env.add_keyword("author", || literal(new_signature("Test User", "x@y")));
insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User <x@y>");
insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"x@y");
insta::assert_snapshot!(env.render_ok("author.email().local()"), @"x");
insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"y");
env.add_keyword("author", || {
literal(new_signature("", "test.user@example.com"))
});
insta::assert_snapshot!(env.render_ok(r#"author"#), @"<test.user@example.com>");
insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"");
insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"test.user@example.com");
env.add_keyword("author", || literal(new_signature("Test User", "")));
insta::assert_snapshot!(env.render_ok(r#"author"#), @"Test User");
insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"Test User");
insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"");
insta::assert_snapshot!(env.render_ok("author.email().local()"), @"");
insta::assert_snapshot!(env.render_ok("author.email().domain()"), @"");
env.add_keyword("author", || literal(new_signature("", "")));
insta::assert_snapshot!(env.render_ok(r#"author"#), @"");
insta::assert_snapshot!(env.render_ok(r#"author.name()"#), @"");
insta::assert_snapshot!(env.render_ok(r#"author.email()"#), @"");
}
#[test]
fn test_size_hint_method() {
let mut env = TestTemplateEnv::new();
env.add_keyword("unbounded", || literal((5, None)));
insta::assert_snapshot!(env.render_ok(r#"unbounded.lower()"#), @"5");
insta::assert_snapshot!(env.render_ok(r#"unbounded.upper()"#), @"");
insta::assert_snapshot!(env.render_ok(r#"unbounded.exact()"#), @"");
insta::assert_snapshot!(env.render_ok(r#"unbounded.zero()"#), @"false");
env.add_keyword("bounded", || literal((0, Some(10))));
insta::assert_snapshot!(env.render_ok(r#"bounded.lower()"#), @"0");
insta::assert_snapshot!(env.render_ok(r#"bounded.upper()"#), @"10");
insta::assert_snapshot!(env.render_ok(r#"bounded.exact()"#), @"");
insta::assert_snapshot!(env.render_ok(r#"bounded.zero()"#), @"false");
env.add_keyword("zero", || literal((0, Some(0))));
insta::assert_snapshot!(env.render_ok(r#"zero.lower()"#), @"0");
insta::assert_snapshot!(env.render_ok(r#"zero.upper()"#), @"0");
insta::assert_snapshot!(env.render_ok(r#"zero.exact()"#), @"0");
insta::assert_snapshot!(env.render_ok(r#"zero.zero()"#), @"true");
}
#[test]
fn test_timestamp_method() {
let mut env = TestTemplateEnv::new();
env.add_keyword("now", || literal(Timestamp::now()));
env.add_keyword("t0", || literal(new_timestamp(0, 0)));
env.add_keyword("t0_plus1", || literal(new_timestamp(0, 60)));
env.add_keyword("tmax", || literal(new_timestamp(i64::MAX, 0)));
// Unformattable timestamp
insta::assert_snapshot!(env.render_ok("tmax"),
@"<Error: Out-of-range date>");
insta::assert_snapshot!(
env.render_ok(r#"t0.format("%Y%m%d %H:%M:%S")"#),
@"19700101 00:00:00");
// Invalid format string
insta::assert_snapshot!(env.parse_err(r#"t0.format("%_")"#), @r#"
--> 1:11
|
1 | t0.format("%_")
| ^--^
|
= Invalid time format
"#);
// Dynamic format string
env.add_dynamic_keyword("good_dyn_format", || "%Y%m".to_owned());
env.add_dynamic_keyword("bad_dyn_format", || "%_".to_owned());
insta::assert_snapshot!(env.render_ok("t0.format(good_dyn_format)"), @"197001");
insta::assert_snapshot!(
env.render_ok("t0.format(bad_dyn_format)"),
@"<Error: Invalid time format: %_>");
// Literal alias expansion
env.add_alias("time_format", r#""%Y-%m-%d""#);
env.add_alias("bad_time_format", r#""%_""#);
insta::assert_snapshot!(env.render_ok(r#"t0.format(time_format)"#), @"1970-01-01");
insta::assert_snapshot!(env.parse_err(r#"t0.format(bad_time_format)"#), @r#"
--> 1:11
|
1 | t0.format(bad_time_format)
| ^-------------^
|
= In alias `bad_time_format`
--> 1:1
|
1 | "%_"
| ^--^
|
= Invalid time format
"#);
insta::assert_snapshot!(env.render_ok("t0_plus1.utc()"), @"1970-01-01 00:00:00.000 +00:00");
// TODO: exercise ago() and local() deterministically
// Just make sure these methods work for now
assert!(!env.render_ok("now.ago()").is_empty());
assert!(!env.render_ok("now.local()").is_empty());
insta::assert_snapshot!(env.render_ok("t0.after('1969')"), @"true");
insta::assert_snapshot!(env.render_ok("t0.before('1969')"), @"false");
insta::assert_snapshot!(env.render_ok("t0.after('now')"), @"false");
insta::assert_snapshot!(env.render_ok("t0.before('now')"), @"true");
insta::assert_snapshot!(env.parse_err("t0.before('invalid')"), @"
--> 1:11
|
1 | t0.before('invalid')
| ^-------^
|
= Invalid date pattern
expected unsupported identifier as position 0..7
");
insta::assert_snapshot!(env.parse_err("t0.before('invalid')"), @"
--> 1:11
|
1 | t0.before('invalid')
| ^-------^
|
= Invalid date pattern
expected unsupported identifier as position 0..7
");
// Can only compare timestamps against string literals
insta::assert_snapshot!(env.parse_err("t0.after(t0)"), @"
--> 1:10
|
1 | t0.after(t0)
| ^^
|
= Expected string literal
");
insta::assert_snapshot!(env.parse_err("t0.before(t0)"), @"
--> 1:11
|
1 | t0.before(t0)
| ^^
|
= Expected string literal
");
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");
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");
insta::assert_snapshot!(env.parse_err("t0.since(false)"), @"
--> 1:10
|
1 | t0.since(false)
| ^---^
|
= Expected expression of type `Timestamp`, but actual type is `Boolean`
");
}
#[test]
fn test_timestamp_range_method() {
let mut env = TestTemplateEnv::new();
env.add_keyword("instant", || {
literal(TimestampRange {
start: new_timestamp(0, 0),
end: new_timestamp(0, 0),
})
});
env.add_keyword("one_msec", || {
literal(TimestampRange {
start: new_timestamp(0, 0),
end: new_timestamp(1, -60),
})
});
insta::assert_snapshot!(
env.render_ok("instant.start().format('%Y%m%d %H:%M:%S %Z')"),
@"19700101 00:00:00 +00:00");
insta::assert_snapshot!(
env.render_ok("one_msec.end().format('%Y%m%d %H:%M:%S %Z')"),
@"19691231 23:00:00 -01:00");
insta::assert_snapshot!(
env.render_ok("instant.duration()"), @"less than a microsecond");
insta::assert_snapshot!(
env.render_ok("one_msec.duration()"), @"1 millisecond");
}
#[test]
fn test_fill_function() {
let mut env = TestTemplateEnv::new();
env.add_color("error", crossterm::style::Color::DarkRed);
insta::assert_snapshot!(
env.render_ok(r#"fill(20, "The quick fox jumps over the " ++
label("error", "lazy") ++ " dog\n")"#),
@"
The quick fox jumps
over the lazy dog
");
// A low value will not chop words, but can chop a label by words
insta::assert_snapshot!(
env.render_ok(r#"fill(9, "Longlonglongword an some short words " ++
label("error", "longlonglongword and short words") ++
" back out\n")"#),
@"
Longlonglongword
an some
short
words
longlonglongword
and short
words
back out
");
// Filling to 0 means breaking at every word
insta::assert_snapshot!(
env.render_ok(r#"fill(0, "The quick fox jumps over the " ++
label("error", "lazy") ++ " dog\n")"#),
@"
The
quick
fox
jumps
over
the
lazy
dog
");
// Filling to -0 is the same as 0
insta::assert_snapshot!(
env.render_ok(r#"fill(-0, "The quick fox jumps over the " ++
label("error", "lazy") ++ " dog\n")"#),
@"
The
quick
fox
jumps
over
the
lazy
dog
");
// Filling to negative width is an error
insta::assert_snapshot!(
env.render_ok(r#"fill(-10, "The quick fox jumps over the " ++
label("error", "lazy") ++ " dog\n")"#),
@"<Error: out of range integral type conversion attempted>");
// Word-wrap, then indent
insta::assert_snapshot!(
env.render_ok(r#""START marker to help insta\n" ++
indent(" ", fill(20, "The quick fox jumps over the " ++
label("error", "lazy") ++ " dog\n"))"#),
@"
START marker to help insta
The quick fox jumps
over the lazy dog
");
// Word-wrap indented (no special handling for leading spaces)
insta::assert_snapshot!(
env.render_ok(r#""START marker to help insta\n" ++
fill(20, indent(" ", "The quick fox jumps over the " ++
label("error", "lazy") ++ " dog\n"))"#),
@"
START marker to help insta
The quick fox
jumps over the lazy
dog
");
}
#[test]
fn test_replace_function() {
let mut env = TestTemplateEnv::new();
env.add_color("error", crossterm::style::Color::DarkRed);
env.add_color("warning", crossterm::style::Color::DarkYellow);
// Can replace a single match.
insta::assert_snapshot!(
env.render_ok(r#"replace("Hi", label("error", "Hi world"), |_| "Hello")"#),
@"Hello world");
// Can replace multiple matches.
insta::assert_snapshot!(
env.render_ok(r#"replace("Hi", label("error", "Hi Hi world"), |_| "Hello")"#),
@"Hello Hello world");
// Text passed to the replacement function does not have its formatting
// preserved.
insta::assert_snapshot!(
env.render_ok(r#"replace("ello w",
label("error", "Hello") ++ " " ++ label("warning", "world"),
|c| c.get(0).upper())"#),
@"HELLO World");
// Access capture groups by index.
insta::assert_snapshot!(
env.render_ok(r#"replace(regex-i:"(ello) (w)",
label("error", "HELLO") ++ " " ++ label("warning", "world"),
|c| c.get(1).lower() ++ " " ++ c.get(2).upper())"#),
@"Hello World");
// Access capture groups by name.
insta::assert_snapshot!(
env.render_ok(r#"replace(regex-i:'(?P<h>ello) (?P<w>w)',
label("error", "HELLO") ++ " " ++ label("warning", "world"),
|c| c.name("h").lower() ++ " " ++ c.name("w").upper())"#),
@"Hello World");
// Access optional capture groups by index.
insta::assert_snapshot!(
env.render_ok(r#"replace(regex-i:"(hello) (b)?",
label("error", "HELLO") ++ " " ++ label("warning", "world"),
|c| c.get(1).lower() ++ " " ++ c.get(2).upper())"#),
@"hello world");
// Access optional capture groups by name.
insta::assert_snapshot!(
env.render_ok(r#"replace(regex-i:'(?P<h>ello) (?P<b>b)',
label("error", "HELLO") ++ " " ++ label("warning", "world"),
|c| c.name("h").lower() ++ " " ++ c.name("b").upper())"#),
@"HELLO world");
// .len() indicates number of capture groups (including full match).
insta::assert_snapshot!(
env.render_ok(r#"replace(regex:"(a)(b)", "ab", |c| c.len())"#),
@"3");
// Out-of-bounds index throws an error.
insta::assert_snapshot!(
env.render_ok(r#"replace("Hi", "Hi world", |c| c.get(99))"#),
@"<Error: Could not get capture group with index 99> world");
// Unknown named group throws an error.
insta::assert_snapshot!(
env.render_ok(r#"replace("Hi", "Hi world", |c| c.name("no_such_group"))"#),
@"<Error: Could not get capture group with name no_such_group> world");
// Invalid UTF-8 in a capture group isn't an error.
insta::assert_snapshot!(
env.render_ok(r#"replace(regex:'(?-u)^(.{3})(.)$', "🥺", |c| json(c.get(1)))"#),
@"[240,159,165]");
}
#[test]
fn test_indent_function() {
let mut env = TestTemplateEnv::new();
env.add_color("error", crossterm::style::Color::DarkRed);
env.add_color("warning", crossterm::style::Color::DarkYellow);
env.add_color("hint", crossterm::style::Color::DarkCyan);
// Empty line shouldn't be indented. Not using insta here because we test
// whitespace existence.
assert_eq!(env.render_ok(r#"indent("__", "")"#), "");
assert_eq!(env.render_ok(r#"indent("__", "\n")"#), "\n");
assert_eq!(env.render_ok(r#"indent("__", "a\n\nb")"#), "__a\n\n__b");
// "\n" at end of labeled text
insta::assert_snapshot!(
env.render_ok(r#"indent("__", label("error", "a\n") ++ label("warning", "b\n"))"#),
@"
__a
__b
");
// "\n" in labeled text
insta::assert_snapshot!(
env.render_ok(r#"indent("__", label("error", "a") ++ label("warning", "b\nc"))"#),
@"
__ab
__c
");
// Labeled prefix + unlabeled content
insta::assert_snapshot!(
env.render_ok(r#"indent(label("error", "XX"), "a\nb\n")"#),
@"
XXa
XXb
");
// Nested indent, silly but works
insta::assert_snapshot!(
env.render_ok(r#"indent(label("hint", "A"),
label("warning", indent(label("hint", "B"),
label("error", "x\n") ++ "y")))"#),
@"
ABx
ABy
");
}
#[test]
fn test_pad_function() {
let mut env = TestTemplateEnv::new();
env.add_keyword("bad_string", || new_error_property::<String>("Bad"));
env.add_color("red", crossterm::style::Color::Red);
env.add_color("cyan", crossterm::style::Color::DarkCyan);
// Default fill_char is ' '
insta::assert_snapshot!(
env.render_ok(r"'{' ++ pad_start(5, label('red', 'foo')) ++ '}'"),
@"{ foo}");
insta::assert_snapshot!(
env.render_ok(r"'{' ++ pad_end(5, label('red', 'foo')) ++ '}'"),
@"{foo }");
insta::assert_snapshot!(
env.render_ok(r"'{' ++ pad_centered(5, label('red', 'foo')) ++ '}'"),
@"{ foo }");
// Labeled fill char
insta::assert_snapshot!(
env.render_ok(r"pad_start(5, label('red', 'foo'), fill_char=label('cyan', '='))"),
@"==foo");
insta::assert_snapshot!(
env.render_ok(r"pad_end(5, label('red', 'foo'), fill_char=label('cyan', '='))"),
@"foo==");
insta::assert_snapshot!(
env.render_ok(r"pad_centered(5, label('red', 'foo'), fill_char=label('cyan', '='))"),
@"=foo=");
// Error in fill char: the output looks odd (because the error message
// isn't 1-width character), but is still readable.
insta::assert_snapshot!(
env.render_ok(r"pad_start(3, 'foo', fill_char=bad_string)"),
@"foo");
insta::assert_snapshot!(
env.render_ok(r"pad_end(5, 'foo', fill_char=bad_string)"),
@"foo<<Error: Error: Bad>Bad>");
insta::assert_snapshot!(
env.render_ok(r"pad_centered(5, 'foo', fill_char=bad_string)"),
@"<Error: Bad>foo<Error: Bad>");
// Invalid pad width is not a parse error
insta::assert_snapshot!(
env.render_ok("pad_start(-1, 'foo')"),
@"<Error: out of range integral type conversion attempted>");
}
#[test]
fn test_hash_function() {
let mut env = TestTemplateEnv::new();
env.add_color("red", crossterm::style::Color::Red);
// hash is currently of stringified content
// NOTE: hash algo and per-type behavior are not codified requirements
assert_eq!(env.render_ok("hash(false)"), env.render_ok("hash('false')"));
assert_eq!(env.render_ok("hash(0)"), env.render_ok("hash('0')"));
assert_eq!(
env.render_ok("hash(0)"),
env.render_ok("hash(label('red', '0'))")
);
}
#[test]
fn test_truncate_function() {
let mut env = TestTemplateEnv::new();
env.add_color("red", crossterm::style::Color::Red);
insta::assert_snapshot!(
env.render_ok(r"truncate_start(2, label('red', 'foobar')) ++ 'baz'"),
@"arbaz");
insta::assert_snapshot!(
env.render_ok("truncate_start(5, 'foo', 'bar')"), @"foo");
insta::assert_snapshot!(
env.render_ok("truncate_start(9, 'foobarbazquux', 'dotdot')"), @"dotdotuux");
insta::assert_snapshot!(
env.render_ok(r"truncate_end(2, label('red', 'foobar')) ++ 'baz'"),
@"fobaz");
insta::assert_snapshot!(
env.render_ok("truncate_end(5, 'foo', 'bar')"), @"foo");
insta::assert_snapshot!(
env.render_ok("truncate_end(9, 'foobarbazquux', 'dotdot')"), @"foodotdot");
// invalid truncate width is not a parse error
insta::assert_snapshot!(
env.render_ok("truncate_end(-1, 'foo')"),
@"<Error: out of range integral type conversion attempted>");
}
#[test]
fn test_label_function() {
let mut env = TestTemplateEnv::new();
env.add_keyword("empty", || literal(true));
env.add_color("error", crossterm::style::Color::DarkRed);
env.add_color("warning", crossterm::style::Color::DarkYellow);
// Literal
insta::assert_snapshot!(
env.render_ok(r#"label("error", "text")"#),
@"text");
// Evaluated property
insta::assert_snapshot!(
env.render_ok(r#"label("error".first_line(), "text")"#),
@"text");
// Property evaluation error
insta::assert_snapshot!(
env.render_ok("label(fill(-1, 'foo'), 'text')"),
@"<Error: out of range integral type conversion attempted>");
// Template
insta::assert_snapshot!(
env.render_ok(r#"label(if(empty, "error", "warning"), "text")"#),
@"text");
}
#[test]
fn test_raw_escape_sequence_function_strip_labels() {
let mut env = TestTemplateEnv::new();
env.add_color("error", crossterm::style::Color::DarkRed);
env.add_color("warning", crossterm::style::Color::DarkYellow);
insta::assert_snapshot!(
env.render_ok(r#"raw_escape_sequence(label("error warning", "text"))"#),
@"text",
);
}
#[test]
fn test_raw_escape_sequence_function_ansi_escape() {
let env = TestTemplateEnv::new();
// Sanitize ANSI escape without raw_escape_sequence
insta::assert_snapshot!(env.render_ok(r#""\e""#), @"");
insta::assert_snapshot!(env.render_ok(r#""\x1b""#), @"");
insta::assert_snapshot!(env.render_ok(r#""\x1B""#), @"");
insta::assert_snapshot!(
env.render_ok(r#""]8;;"
++ "http://example.com"
++ "\e\\"
++ "Example"
++ "\x1b]8;;\x1B\\""#),
@r"␛]8;;http://example.com␛\Example␛]8;;␛\");
// Don't sanitize ANSI escape with raw_escape_sequence
insta::assert_snapshot!(env.render_ok(r#"raw_escape_sequence("\e")"#), @"");
insta::assert_snapshot!(env.render_ok(r#"raw_escape_sequence("\x1b")"#), @"");
insta::assert_snapshot!(env.render_ok(r#"raw_escape_sequence("\x1B")"#), @"");
insta::assert_snapshot!(
env.render_ok(r#"raw_escape_sequence("]8;;"
++ "http://example.com"
++ "\e\\"
++ "Example"
++ "\x1b]8;;\x1B\\")"#),
@r"]8;;http://example.com\Example]8;;\");
}
#[test]
fn test_hyperlink_function_with_color() {
let mut env = TestTemplateEnv::new();
env.add_keyword("bad_string", || new_error_property::<String>("Bad"));
// With ColorFormatter, hyperlink emits OSC 8 escape sequences
insta::assert_snapshot!(
env.render_ok(r#"hyperlink("http://example.com", "Example")"#),
@r"]8;;http://example.com\Example]8;;\");
insta::assert_snapshot!(
env.render_ok(r#"hyperlink(bad_string, "Example")"#),
@"<Error: Bad>");
}
#[test]
fn test_hyperlink_function_without_color() {
let env = TestTemplateEnv::new();
// With PlainTextFormatter, hyperlink shows just the text
insta::assert_snapshot!(
env.render_plain(r#"hyperlink("http://example.com", "Example")"#),
@"Example");
}
#[test]
fn test_hyperlink_function_custom_fallback() {
let env = TestTemplateEnv::new();
// Custom fallback is used when not outputting to color terminal
insta::assert_snapshot!(
env.render_plain(r#"hyperlink("http://example.com", "Example", "URL: http://example.com")"#),
@"URL: http://example.com");
}
#[test]
fn test_hyperlink_function_stringify() {
let env = TestTemplateEnv::new();
// stringify() strips hyperlink to just text
insta::assert_snapshot!(
env.render_ok(r#"stringify(hyperlink("http://example.com", "Example"))"#),
@"Example");
// stringify then can be manipulated as plain text
insta::assert_snapshot!(
env.render_ok(r#"stringify(hyperlink("http://example.com", "Example")).upper()"#),
@"EXAMPLE");
}
#[test]
fn test_hyperlink_function_with_separate() {
let env = TestTemplateEnv::new();
// separate() uses FormatRecorder internally; hyperlinks are preserved
insta::assert_snapshot!(
env.render_ok(r#"separate(" | ", hyperlink("http://a.com", "A"), hyperlink("http://b.com", "B"))"#),
@r"]8;;http://a.com\A]8;;\ | ]8;;http://b.com\B]8;;\");
}
#[test]
fn test_hyperlink_function_with_coalesce() {
let env = TestTemplateEnv::new();
// coalesce() uses FormatRecorder; hyperlinks are preserved
insta::assert_snapshot!(
env.render_ok(r#"coalesce(hyperlink("http://example.com", "Link"), "fallback")"#),
@r"]8;;http://example.com\Link]8;;\");
// Falls back to second when hyperlink text is empty
insta::assert_snapshot!(
env.render_ok(r#"coalesce(hyperlink("http://example.com", ""), "fallback")"#),
@"fallback");
}
#[test]
fn test_hyperlink_function_with_if() {
let env = TestTemplateEnv::new();
// if() does not use FormatRecorder; hyperlinks work directly
insta::assert_snapshot!(
env.render_ok(r#"if(true, hyperlink("http://example.com", "Yes"), "No")"#),
@r"]8;;http://example.com\Yes]8;;\");
insta::assert_snapshot!(
env.render_ok(r#"if(false, "Yes", hyperlink("http://example.com", "No"))"#),
@r"]8;;http://example.com\No]8;;\");
}
#[test]
fn test_hyperlink_function_plain_with_separate() {
let env = TestTemplateEnv::new();
// When rendering plain, hyperlinks should fall back to text
insta::assert_snapshot!(
env.render_plain(r#"separate(" | ", hyperlink("http://a.com", "A"), hyperlink("http://b.com", "B"))"#),
@"A | B");
}
#[test]
fn test_stringify_function() {
let mut env = TestTemplateEnv::new();
env.add_keyword("none_i64", || literal(None::<i64>));
env.add_keyword("ascii_bstr", || literal(BString::from("foo")));
env.add_keyword("odd_bstr", || literal(BString::from(b"\x80")));
env.add_color("error", crossterm::style::Color::DarkRed);
insta::assert_snapshot!(env.render_ok("stringify(false)"), @"false");
insta::assert_snapshot!(env.render_ok("stringify(42).len()"), @"2");
insta::assert_snapshot!(env.render_ok("stringify(none_i64)"), @"");
insta::assert_snapshot!(env.render_ok("stringify(ascii_bstr)"), @"foo");
insta::assert_snapshot!(
env.render_ok("stringify(odd_bstr)"),
@"<Error: invalid utf-8 sequence of 1 bytes from index 0>");
insta::assert_snapshot!(env.render_ok("stringify(label('error', 'text'))"), @"text");
}
#[test]
fn test_json_function() {
let mut env = TestTemplateEnv::new();
env.add_keyword("none_i64", || literal(None::<i64>));
env.add_keyword("ascii_bstr", || literal(BString::from("foo")));
env.add_keyword("string_list", || {
literal(vec!["foo".to_owned(), "bar".to_owned()])
});
env.add_keyword("config_value_table", || {
literal(ConfigValue::from_iter([("foo", "bar")]))
});
env.add_keyword("some_cfgval", || literal(Some(ConfigValue::from(1))));
env.add_keyword("none_cfgval", || literal(None::<ConfigValue>));
env.add_keyword("signature", || {
literal(Signature {
name: "Test User".to_owned(),
email: "test.user@example.com".to_owned(),
timestamp: Timestamp {
timestamp: MillisSinceEpoch(0),
tz_offset: 0,
},
})
});
env.add_keyword("email", || literal(Email("foo@bar".to_owned())));
env.add_keyword("size_hint", || literal((5, None)));
env.add_keyword("timestamp", || {
literal(Timestamp {
timestamp: MillisSinceEpoch(0),
tz_offset: 0,
})
});
env.add_keyword("timestamp_range", || {
literal(TimestampRange {
start: Timestamp {
timestamp: MillisSinceEpoch(0),
tz_offset: 0,
},
end: Timestamp {
timestamp: MillisSinceEpoch(86_400_000),
tz_offset: -60,
},
})
});
insta::assert_snapshot!(env.render_ok(r#"json(ascii_bstr)"#), @"[102,111,111]");
insta::assert_snapshot!(env.render_ok(r#"json('"quoted"')"#), @r#""\"quoted\"""#);
insta::assert_snapshot!(env.render_ok(r#"json(string_list)"#), @r#"["foo","bar"]"#);
insta::assert_snapshot!(env.render_ok("json(false)"), @"false");
insta::assert_snapshot!(env.render_ok("json(42)"), @"42");
insta::assert_snapshot!(env.render_ok("json(none_i64)"), @"null");
insta::assert_snapshot!(env.render_ok(r#"json(config_value_table)"#), @r#"{"foo":"bar"}"#);
insta::assert_snapshot!(env.render_ok(r"json(some_cfgval)"), @"1");
insta::assert_snapshot!(env.render_ok(r"json(none_cfgval)"), @"null");
insta::assert_snapshot!(env.render_ok("json(email)"), @r#""foo@bar""#);
insta::assert_snapshot!(
env.render_ok("json(signature)"),
@r#"{"name":"Test User","email":"test.user@example.com","timestamp":"1970-01-01T00:00:00Z"}"#);
insta::assert_snapshot!(env.render_ok("json(size_hint)"), @"[5,null]");
insta::assert_snapshot!(env.render_ok("json(timestamp)"), @r#""1970-01-01T00:00:00Z""#);
insta::assert_snapshot!(
env.render_ok("json(timestamp_range)"),
@r#"{"start":"1970-01-01T00:00:00Z","end":"1970-01-01T23:00:00-01:00"}"#);
// AnyList is serializable if the inner type is.
insta::assert_snapshot!(env.render_ok(r#"json(string_list.map(|s| s))"#), @r#"["foo","bar"]"#);
insta::assert_snapshot!(env.render_ok(r#"json(string_list.map(|s| size_hint))"#), @"[[5,null],[5,null]]");
// Any is serializable if the inner types are.
insta::assert_snapshot!(env.render_ok(r#"json(if(true, email, timestamp))"#), @r#""foo@bar""#);
insta::assert_snapshot!(env.render_ok(r#"json(if(true, size_hint, config_value_table))"#), @"[5,null]");
// The else case missing does prevents the resulting Any expression
// from being serializable.
insta::assert_snapshot!(env.parse_err(r#"json(if(true, email))"#), @r###"
--> 1:6
|
1 | json(if(true, email))
| ^-------------^
|
= Expected expression of type `Serialize`, but actual type is `Any`
"###);
insta::assert_snapshot!(env.parse_err(r#"json(if(false, email))"#), @r###"
--> 1:6
|
1 | json(if(false, email))
| ^--------------^
|
= Expected expression of type `Serialize`, but actual type is `Any`
"###);
}
#[test]
fn test_coalesce_function() {
let mut env = TestTemplateEnv::new();
env.add_keyword("bad_string", || new_error_property::<String>("Bad"));
env.add_keyword("empty_string", || literal("".to_owned()));
env.add_keyword("non_empty_string", || literal("a".to_owned()));
insta::assert_snapshot!(env.render_ok(r#"coalesce()"#), @"");
insta::assert_snapshot!(env.render_ok(r#"coalesce("")"#), @"");
insta::assert_snapshot!(env.render_ok(r#"coalesce("", "a", "", "b")"#), @"a");
insta::assert_snapshot!(
env.render_ok(r#"coalesce(empty_string, "", non_empty_string)"#), @"a");
// "false" is not empty
insta::assert_snapshot!(env.render_ok(r#"coalesce(false, true)"#), @"false");
// Error is not empty
insta::assert_snapshot!(env.render_ok(r#"coalesce(bad_string, "a")"#), @"<Error: Bad>");
// but can be short-circuited
insta::assert_snapshot!(env.render_ok(r#"coalesce("a", bad_string)"#), @"a");
// Keyword arguments are rejected.
insta::assert_snapshot!(env.parse_err(r#"coalesce("a", value2="b")"#), @r#"
--> 1:15
|
1 | coalesce("a", value2="b")
| ^--------^
|
= Function `coalesce`: Unexpected keyword arguments
"#);
}
#[test]
fn test_concat_function() {
let mut env = TestTemplateEnv::new();
env.add_keyword("empty", || literal(true));
env.add_keyword("hidden", || literal(false));
env.add_color("empty", crossterm::style::Color::DarkGreen);
env.add_color("error", crossterm::style::Color::DarkRed);
env.add_color("warning", crossterm::style::Color::DarkYellow);
insta::assert_snapshot!(env.render_ok(r#"concat()"#), @"");
insta::assert_snapshot!(
env.render_ok(r#"concat(hidden, empty)"#),
@"falsetrue");
insta::assert_snapshot!(
env.render_ok(r#"concat(label("error", ""), label("warning", "a"), "b")"#),
@"ab");
// Keyword arguments are rejected.
insta::assert_snapshot!(env.parse_err(r#"concat("a", value2="b")"#), @r#"
--> 1:13
|
1 | concat("a", value2="b")
| ^--------^
|
= Function `concat`: Unexpected keyword arguments
"#);
}
#[test]
fn test_join_function() {
let mut env = TestTemplateEnv::new();
env.add_keyword("description", || literal("".to_owned()));
env.add_keyword("empty", || literal(true));
env.add_keyword("hidden", || literal(false));
env.add_color("empty", crossterm::style::Color::DarkGreen);
env.add_color("error", crossterm::style::Color::DarkRed);
env.add_color("warning", crossterm::style::Color::DarkYellow);
// Template literals.
insta::assert_snapshot!(env.render_ok(r#"join(",")"#), @"");
insta::assert_snapshot!(env.render_ok(r#"join(",", "")"#), @"");
insta::assert_snapshot!(env.render_ok(r#"join(",", "a")"#), @"a");
insta::assert_snapshot!(env.render_ok(r#"join(",", "a", "b")"#), @"a,b");
insta::assert_snapshot!(env.render_ok(r#"join(",", "a", "", "b")"#), @"a,,b");
insta::assert_snapshot!(env.render_ok(r#"join(",", "a", "b", "")"#), @"a,b,");
insta::assert_snapshot!(env.render_ok(r#"join(",", "", "a", "b")"#), @",a,b");
insta::assert_snapshot!(
env.render_ok(r#"join("--", 1, "", true, "test", "")"#),
@"1----true--test--");
// Separator is required.
insta::assert_snapshot!(env.parse_err(r#"join()"#), @"
--> 1:6
|
1 | join()
| ^
|
= Function `join`: Expected at least 1 arguments
");
// Labeled.
insta::assert_snapshot!(
env.render_ok(r#"join(",", label("error", ""), label("warning", "a"), "b")"#),
@",a,b");
insta::assert_snapshot!(
env.render_ok(
r#"join(label("empty", "<>"), label("error", "a"), label("warning", ""), "b")"#),
@"a<><>b");
// List template.
insta::assert_snapshot!(env.render_ok(r#"join(",", "a", ("" ++ ""))"#), @"a,");
insta::assert_snapshot!(env.render_ok(r#"join(",", "a", ("" ++ "b"))"#), @"a,b");
// Nested.
insta::assert_snapshot!(
env.render_ok(r#"join(",", "a", join("|", "", ""))"#), @"a,|");
insta::assert_snapshot!(
env.render_ok(r#"join(",", "a", join("|", "b", ""))"#), @"a,b|");
insta::assert_snapshot!(
env.render_ok(r#"join(",", "a", join("|", "b", "c"))"#), @"a,b|c");
// Keywords.
insta::assert_snapshot!(
env.render_ok(r#"join(",", hidden, description, empty)"#),
@"false,,true");
insta::assert_snapshot!(
env.render_ok(r#"join(hidden, "X", "Y", "Z")"#),
@"XfalseYfalseZ");
insta::assert_snapshot!(
env.render_ok(r#"join(hidden, empty)"#),
@"true");
// Keyword arguments are rejected.
insta::assert_snapshot!(env.parse_err(r#"join(",", "a", arg="b")"#), @r#"
--> 1:16
|
1 | join(",", "a", arg="b")
| ^-----^
|
= Function `join`: Unexpected keyword arguments
"#);
// only size hints cannot be templated / joined
env.add_keyword("str_list", || {
literal(vec!["foo".to_owned(), "bar".to_owned()])
});
env.add_keyword("none_int", || literal(None::<i64>));
env.add_keyword("some_int", || literal(Some(67)));
env.add_keyword("cfg_val", || {
literal(ConfigValue::from_iter([("foo", "bar")]))
});
env.add_keyword("email", || literal(Email("me@example.com".to_owned())));
env.add_keyword("signature", || {
literal(new_signature("User", "user@example.com"))
});
env.add_keyword("size_hint", || literal((10, None)));
env.add_keyword("timestamp", || literal(new_timestamp(0, 0)));
env.add_keyword("timestamp_range", || {
literal(TimestampRange {
start: new_timestamp(0, 0),
end: new_timestamp(0, 0),
})
});
insta::assert_snapshot!(
env.render_ok("join('|', str_list, 42, none_int, some_int)"),
@"foo bar|42||67");
insta::assert_snapshot!(
env.render_ok("join('|', cfg_val, email, signature, if(true, 42), if(false, 42))"),
@r#"{ foo = "bar" }|me@example.com|User <user@example.com>|42|"#);
insta::assert_snapshot!(
env.render_ok("join('|', timestamp, timestamp_range, str_list.map(|x| x))"),
@"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");
assert_matches!(
env.parse_err_kind("join('|', size_hint)"),
TemplateParseErrorKind::Expression(_)
);
}
#[test]
fn test_separate_function() {
let mut env = TestTemplateEnv::new();
env.add_keyword("description", || literal("".to_owned()));
env.add_keyword("empty", || literal(true));
env.add_keyword("hidden", || literal(false));
env.add_color("empty", crossterm::style::Color::DarkGreen);
env.add_color("error", crossterm::style::Color::DarkRed);
env.add_color("warning", crossterm::style::Color::DarkYellow);
insta::assert_snapshot!(env.render_ok(r#"separate(" ")"#), @"");
insta::assert_snapshot!(env.render_ok(r#"separate(" ", "")"#), @"");
insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a")"#), @"a");
insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", "b")"#), @"a b");
insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", "", "b")"#), @"a b");
insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", "b", "")"#), @"a b");
insta::assert_snapshot!(env.render_ok(r#"separate(" ", "", "a", "b")"#), @"a b");
// Labeled
insta::assert_snapshot!(
env.render_ok(r#"separate(" ", label("error", ""), label("warning", "a"), "b")"#),
@"a b");
// List template
insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", ("" ++ ""))"#), @"a");
insta::assert_snapshot!(env.render_ok(r#"separate(" ", "a", ("" ++ "b"))"#), @"a b");
// Nested separate
insta::assert_snapshot!(
env.render_ok(r#"separate(" ", "a", separate("|", "", ""))"#), @"a");
insta::assert_snapshot!(
env.render_ok(r#"separate(" ", "a", separate("|", "b", ""))"#), @"a b");
insta::assert_snapshot!(
env.render_ok(r#"separate(" ", "a", separate("|", "b", "c"))"#), @"a b|c");
// Conditional template
insta::assert_snapshot!(
env.render_ok(r#"separate(" ", "a", if(true, ""))"#), @"a");
insta::assert_snapshot!(
env.render_ok(r#"separate(" ", "a", if(true, "", "f"))"#), @"a");
insta::assert_snapshot!(
env.render_ok(r#"separate(" ", "a", if(false, "t"))"#), @"a");
insta::assert_snapshot!(
env.render_ok(r#"separate(" ", "a", if(false, "t", ""))"#), @"a");
insta::assert_snapshot!(
env.render_ok(r#"separate(" ", "a", if(true, "t", "f"))"#), @"a t");
// Separate keywords
insta::assert_snapshot!(
env.render_ok(r#"separate(" ", hidden, description, empty)"#),
@"false true");
// Keyword as separator
insta::assert_snapshot!(
env.render_ok(r#"separate(hidden, "X", "Y", "Z")"#),
@"XfalseYfalseZ");
// Keyword arguments are rejected.
insta::assert_snapshot!(env.parse_err(r#"separate(" ", "a", value2="b")"#), @r#"
--> 1:20
|
1 | separate(" ", "a", value2="b")
| ^--------^
|
= Function `separate`: Unexpected keyword arguments
"#);
}
#[test]
fn test_surround_function() {
let mut env = TestTemplateEnv::new();
env.add_keyword("lt", || literal("<".to_owned()));
env.add_keyword("gt", || literal(">".to_owned()));
env.add_keyword("content", || literal("content".to_owned()));
env.add_keyword("empty_content", || literal("".to_owned()));
env.add_color("error", crossterm::style::Color::DarkRed);
env.add_color("paren", crossterm::style::Color::Cyan);
insta::assert_snapshot!(env.render_ok(r#"surround("{", "}", "")"#), @"");
insta::assert_snapshot!(env.render_ok(r#"surround("{", "}", "a")"#), @"{a}");
// Labeled
insta::assert_snapshot!(
env.render_ok(
r#"surround(label("paren", "("), label("paren", ")"), label("error", "a"))"#),
@"(a)");
// Keyword
insta::assert_snapshot!(
env.render_ok(r#"surround(lt, gt, content)"#),
@"<content>");
insta::assert_snapshot!(
env.render_ok(r#"surround(lt, gt, empty_content)"#),
@"");
// Conditional template as content
insta::assert_snapshot!(
env.render_ok(r#"surround(lt, gt, if(empty_content, "", "empty"))"#),
@"<empty>");
insta::assert_snapshot!(
env.render_ok(r#"surround(lt, gt, if(empty_content, "not empty", ""))"#),
@"");
}
#[test]
fn test_config_function() {
use jj_lib::config::ConfigLayer;
use jj_lib::config::ConfigSource;
let mut config = StackedConfig::with_defaults();
config
.add_layer(ConfigLayer::parse(ConfigSource::User, "user.name = 'Test User'").unwrap());
config.add_layer(
ConfigLayer::parse(ConfigSource::User, "user.email = 'test@example.com'").unwrap(),
);
let mut env = TestTemplateEnv::with_config(config);
// valid config path
insta::assert_snapshot!(env.render_ok(r#"config("user.name")"#), @"'Test User'");
insta::assert_snapshot!(env.render_ok(r#"config("user.email")"#), @"'test@example.com'");
insta::assert_snapshot!(env.render_ok(r#"config("user")"#), @"{ email = 'test@example.com', name = 'Test User' }");
// nonexistent config path
insta::assert_snapshot!(env.render_ok(r#"config("non.existent")"#), @"");
// conditional on config path existence
insta::assert_snapshot!(env.render_ok(r#"if(config("user.name"), "yes", "no")"#), @"yes");
insta::assert_snapshot!(env.render_ok(r#"if(config("non.existent"), "yes", "no")"#), @"no");
// malformed config path
env.add_alias("bad_config_name", "'user|name'");
insta::assert_snapshot!(env.parse_err("config(bad_config_name)"), @"
--> 1:8
|
1 | config(bad_config_name)
| ^-------------^
|
= In alias `bad_config_name`
--> 1:1
|
1 | 'user|name'
| ^---------^
|
= Failed to parse config name
TOML parse error at line 1, column 5
|
1 | user|name
| ^
invalid unquoted key, expected letters, numbers, `-`, `_`
");
// lookup at parse time
env.add_alias("config_key", r#""name""#);
insta::assert_snapshot!(env.render_ok(r#"config("user." ++ "name")"#), @"'Test User'");
insta::assert_snapshot!(env.render_ok(r#"config("us" ++ "er")"#), @"{ email = 'test@example.com', name = 'Test User' }");
insta::assert_snapshot!(env.render_ok(r#"config("user." ++ config_key)"#), @"'Test User'");
// invalid expression
insta::assert_snapshot!(env.parse_err(r#"config("user." ++)"#), @r#"
--> 1:18
|
1 | config("user." ++)
| ^---
|
= expected <expression>
"#);
insta::assert_snapshot!(env.parse_err(r#"config("user|" ++ "name")"#), @r#"
--> 1:8
|
1 | config("user|" ++ "name")
| ^---------------^
|
= Failed to parse config name
TOML parse error at line 1, column 5
|
1 | user|name
| ^
invalid unquoted key, expected letters, numbers, `-`, `_`
"#);
insta::assert_snapshot!(env.parse_err(r#"config(invalid)"#), @"
--> 1:8
|
1 | config(invalid)
| ^-----^
|
= Keyword `invalid` doesn't exist
");
// dynamic lookup using a keyword that depends on runtime context
env.add_dynamic_keyword("dyn_config_name", || "user.name".to_owned());
insta::assert_snapshot!(
env.render_ok(r#"config(dyn_config_name)"#), @"'Test User'"
);
// dynamic lookup with nonexistent path
env.add_dynamic_keyword("dyn_missing", || "non.existent".to_owned());
insta::assert_snapshot!(env.render_ok(r#"config(dyn_missing)"#), @"");
// dynamic lookup with invalid config path at runtime
env.add_dynamic_keyword("dyn_bad_path", || "user|name".to_owned());
insta::assert_snapshot!(env.render_ok(r#"config(dyn_bad_path)"#), @r"
<Error: TOML parse error at line 1, column 5
|
1 | user|name
| ^
invalid unquoted key, expected letters, numbers, `-`, `_`
>
");
// dynamic lookup where name expression itself fails at runtime
env.add_keyword("bad_string", || new_error_property::<String>("Bad"));
insta::assert_snapshot!(env.render_ok(r#"config(bad_string)"#), @"<Error: Bad>");
}
#[test]
fn test_any_type() {
let mut env = TestTemplateEnv::new();
env.add_keyword("size_hint", || literal((5, None)));
env.add_keyword("size_hint_2", || literal((10, None)));
env.add_keyword("words", || {
literal(vec!["foo".to_owned(), "bar".to_owned()])
});
env.add_color("red", crossterm::style::Color::Red);
// If requires both halves of the statement to support the trait.
insta::assert_snapshot!(env.render_ok(r#"if(true, label("red", "a"), "b")"#), @"a");
insta::assert_snapshot!(env.render_ok(r#"if(false, label("red", "a"), "b")"#), @"b");
insta::assert_snapshot!(env.render_ok(r#"json(if(true, size_hint, size_hint_2))"#), @"[5,null]");
insta::assert_snapshot!(env.render_ok(r#"json(if(false, size_hint, size_hint_2))"#), @"[10,null]");
// If one of the cases does not support Template/Serialize, fail even if
// that case isn't selected.
insta::assert_snapshot!(env.parse_err(r#"if(true, label("red", "a"), size_hint)"#), @r#"
--> 1:1
|
1 | if(true, label("red", "a"), size_hint)
| ^------------------------------------^
|
= Expected expression of type `Template`, but actual type is `Any`
"#);
insta::assert_snapshot!(env.parse_err(r#"json(if(true, size_hint, label("red", "a")))"#), @r#"
--> 1:6
|
1 | json(if(true, size_hint, label("red", "a")))
| ^------------------------------------^
|
= Expected expression of type `Serialize`, but actual type is `Any`
"#);
// The `join` method should not be available on `Any`.
insta::assert_snapshot!(env.parse_err(r#"if(true,words,words).join(", ")"#), @r#"
--> 1:22
|
1 | if(true,words,words).join(", ")
| ^--^
|
= Method `join` doesn't exist for type `Any`
"#);
}
#[test]
fn test_any_list_type() {
let mut env = TestTemplateEnv::new();
env.add_keyword("words", || {
literal(vec!["foo".to_owned(), "bar".to_owned()])
});
env.add_keyword("size_hint", || literal((10, None)));
env.add_color("red", crossterm::style::Color::Red);
// Map items are not required to implement both Template and Serialize.
insta::assert_snapshot!(env.render_ok(
r#"words.map(|x| label("red", x))"#),
@"foo bar");
insta::assert_snapshot!(env.render_ok(
r#"words.map(|x| label("red", x)).join(",")"#),
@"foo,bar");
insta::assert_snapshot!(env.render_ok(
r#"json(words.map(|x| size_hint))"#),
@"[[10,null],[10,null]]");
// You cannot use the result if when the trait is not implemented.
insta::assert_snapshot!(env.parse_err(r#"words.map(|x| size_hint)"#), @r#"
--> 1:1
|
1 | words.map(|x| size_hint)
| ^----------------------^
|
= Expected expression of type `Template`, but actual type is `AnyList`
"#);
insta::assert_snapshot!(env.parse_err(r#"words.map(|x| size_hint).join(",")"#), @r#"
--> 1:26
|
1 | words.map(|x| size_hint).join(",")
| ^--^
|
= Expected expression of type `Template`, but actual type is `AnyList`
"#);
insta::assert_snapshot!(env.parse_err(r#"json(words.map(|x| label("red", x)))"#), @r#"
--> 1:6
|
1 | json(words.map(|x| label("red", x)))
| ^----------------------------^
|
= Expected expression of type `Serialize`, but actual type is `AnyList`
"#);
}
}