// 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::collections::HashMap; use itertools::Itertools as _;use jj_lib::backend::{Signature, Timestamp}; use crate::template_parser::{ self, ExpressionKind, ExpressionNode, FunctionCallNode, MethodCallNode, TemplateParseError, TemplateParseResult,};use crate::templater::{ ConcatTemplate, ConditionalTemplate, IntoTemplate, LabelTemplate, ListPropertyTemplate, ListTemplate, Literal, PlainTextFormattedProperty, PropertyPlaceholder, ReformatTemplate, SeparateTemplate, Template, TemplateFunction, TemplateProperty, TimestampRange,};use crate::{text_util, time_util}; /// Callbacks to build language-specific evaluation objects from AST nodes.pub trait TemplateLanguage<'a> { type Context: 'a; type Property: IntoTemplateProperty<'a, Self::Context>; fn wrap_string( &self, property: impl TemplateProperty<Self::Context, Output = String> + 'a, ) -> Self::Property; fn wrap_string_list( &self, property: impl TemplateProperty<Self::Context, Output = Vec<String>> + 'a, ) -> Self::Property; fn wrap_boolean( &self, property: impl TemplateProperty<Self::Context, Output = bool> + 'a, ) -> Self::Property; fn wrap_integer( &self, property: impl TemplateProperty<Self::Context, Output = i64> + 'a, ) -> Self::Property; fn wrap_signature( &self, property: impl TemplateProperty<Self::Context, Output = Signature> + 'a, ) -> Self::Property; fn wrap_timestamp( &self, property: impl TemplateProperty<Self::Context, Output = Timestamp> + 'a, ) -> Self::Property; fn wrap_timestamp_range( &self, property: impl TemplateProperty<Self::Context, Output = TimestampRange> + 'a, ) -> Self::Property; fn wrap_template(&self, template: Box<dyn Template<Self::Context> + 'a>) -> Self::Property; fn wrap_list_template( &self, template: Box<dyn ListTemplate<Self::Context> + 'a>, ) -> Self::Property; fn build_keyword(&self, name: &str, span: pest::Span) -> TemplateParseResult<Self::Property>; fn build_method( &self, build_ctx: &BuildContext<Self::Property>, property: Self::Property, function: &FunctionCallNode, ) -> TemplateParseResult<Self::Property>;} /// Implements `TemplateLanguage::wrap_<type>()` functions.////// - `impl_core_wrap_property_fns('a)` for `CoreTemplatePropertyKind`,/// - `impl_core_wrap_property_fns('a, MyKind::Core)` for `MyKind::Core(..)`.macro_rules! impl_core_wrap_property_fns { ($a:lifetime) => { $crate::template_builder::impl_core_wrap_property_fns!($a, std::convert::identity); }; ($a:lifetime, $outer:path) => { $crate::template_builder::impl_wrap_property_fns!( $a, $crate::template_builder::CoreTemplatePropertyKind, $outer, { wrap_string(String) => String, wrap_string_list(Vec<String>) => StringList, wrap_boolean(bool) => Boolean, wrap_integer(i64) => Integer, wrap_signature(jj_lib::backend::Signature) => Signature, wrap_timestamp(jj_lib::backend::Timestamp) => Timestamp, wrap_timestamp_range($crate::templater::TimestampRange) => TimestampRange, } ); fn wrap_template( &self, template: Box<dyn $crate::templater::Template<Self::Context> + $a>, ) -> Self::Property { use $crate::template_builder::CoreTemplatePropertyKind as Kind; $outer(Kind::Template(template)) } fn wrap_list_template( &self, template: Box<dyn $crate::templater::ListTemplate<Self::Context> + $a>, ) -> Self::Property { use $crate::template_builder::CoreTemplatePropertyKind as Kind; $outer(Kind::ListTemplate(template)) } };} macro_rules! impl_wrap_property_fns { ($a:lifetime, $kind:path, $outer:path, { $( $func:ident($ty:ty) => $var:ident, )+ }) => { $( fn $func( &self, property: impl $crate::templater::TemplateProperty< Self::Context, Output = $ty> + $a, ) -> Self::Property { use $kind as Kind; // https://github.com/rust-lang/rust/issues/48067 $outer(Kind::$var(Box::new(property))) } )+ };} pub(crate) use {impl_core_wrap_property_fns, impl_wrap_property_fns}; /// Provides access to basic template property types.pub trait IntoTemplateProperty<'a, C> { fn try_into_boolean(self) -> Option<Box<dyn TemplateProperty<C, Output = bool> + 'a>>; fn try_into_integer(self) -> Option<Box<dyn TemplateProperty<C, Output = i64> + 'a>>; fn try_into_plain_text(self) -> Option<Box<dyn TemplateProperty<C, Output = String> + 'a>>; fn try_into_template(self) -> Option<Box<dyn Template<C> + 'a>>;} pub enum CoreTemplatePropertyKind<'a, I> { String(Box<dyn TemplateProperty<I, Output = String> + 'a>), StringList(Box<dyn TemplateProperty<I, Output = Vec<String>> + 'a>), Boolean(Box<dyn TemplateProperty<I, Output = bool> + 'a>), Integer(Box<dyn TemplateProperty<I, Output = i64> + 'a>), Signature(Box<dyn TemplateProperty<I, Output = Signature> + 'a>), Timestamp(Box<dyn TemplateProperty<I, Output = Timestamp> + 'a>), TimestampRange(Box<dyn TemplateProperty<I, Output = TimestampRange> + 'a>), // Similar to `TemplateProperty<I, Output = Box<dyn Template<()> + 'a>`, but doesn't // capture `I` to produce `Template<()>`. The context `I` would have to be cloned // to convert `Template<I>` to `Template<()>`. Template(Box<dyn Template<I> + 'a>), ListTemplate(Box<dyn ListTemplate<I> + 'a>),} impl<'a, I: 'a> IntoTemplateProperty<'a, I> for CoreTemplatePropertyKind<'a, I> { fn try_into_boolean(self) -> Option<Box<dyn TemplateProperty<I, Output = bool> + 'a>> { match self { CoreTemplatePropertyKind::String(property) => { Some(Box::new(TemplateFunction::new(property, |s| !s.is_empty()))) } CoreTemplatePropertyKind::StringList(property) => { Some(Box::new(TemplateFunction::new(property, |l| !l.is_empty()))) } CoreTemplatePropertyKind::Boolean(property) => Some(property), CoreTemplatePropertyKind::Integer(_) => None, CoreTemplatePropertyKind::Signature(_) => None, CoreTemplatePropertyKind::Timestamp(_) => None, CoreTemplatePropertyKind::TimestampRange(_) => None, // Template types could also be evaluated to boolean, but it's less likely // to apply label() or .map() and use the result as conditional. It's also // unclear whether ListTemplate should behave as a "list" or a "template". CoreTemplatePropertyKind::Template(_) => None, CoreTemplatePropertyKind::ListTemplate(_) => None, } } fn try_into_integer(self) -> Option<Box<dyn TemplateProperty<I, Output = i64> + 'a>> { match self { CoreTemplatePropertyKind::Integer(property) => Some(property), _ => None, } } fn try_into_plain_text(self) -> Option<Box<dyn TemplateProperty<I, Output = String> + 'a>> { match self { CoreTemplatePropertyKind::String(property) => Some(property), _ => { let template = self.try_into_template()?; Some(Box::new(PlainTextFormattedProperty::new(template))) } } } fn try_into_template(self) -> Option<Box<dyn Template<I> + 'a>> { match self { CoreTemplatePropertyKind::String(property) => Some(property.into_template()), CoreTemplatePropertyKind::StringList(property) => Some(property.into_template()), CoreTemplatePropertyKind::Boolean(property) => Some(property.into_template()), CoreTemplatePropertyKind::Integer(property) => Some(property.into_template()), CoreTemplatePropertyKind::Signature(property) => Some(property.into_template()), CoreTemplatePropertyKind::Timestamp(property) => Some(property.into_template()), CoreTemplatePropertyKind::TimestampRange(property) => Some(property.into_template()), CoreTemplatePropertyKind::Template(template) => Some(template), CoreTemplatePropertyKind::ListTemplate(template) => Some(template.into_template()), } }} /// 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![]; Expression { property, labels } } fn with_label(property: P, label: impl Into<String>) -> Self { let labels = vec![label.into()]; Expression { property, labels } } pub fn try_into_boolean<'a, C: 'a>( self, ) -> Option<Box<dyn TemplateProperty<C, Output = bool> + 'a>> where P: IntoTemplateProperty<'a, C>, { self.property.try_into_boolean() } pub fn try_into_integer<'a, C: 'a>( self, ) -> Option<Box<dyn TemplateProperty<C, Output = i64> + 'a>> where P: IntoTemplateProperty<'a, C>, { self.property.try_into_integer() } pub fn try_into_plain_text<'a, C: 'a>( self, ) -> Option<Box<dyn TemplateProperty<C, Output = String> + 'a>> where P: IntoTemplateProperty<'a, C>, { self.property.try_into_plain_text() } pub fn try_into_template<'a, C: 'a>(self) -> Option<Box<dyn Template<C> + 'a>> where P: IntoTemplateProperty<'a, C>, { 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 struct BuildContext<'i, P> { /// Map of functions to create `L::Property`. local_variables: HashMap<&'i str, &'i (dyn Fn() -> P)>,} fn build_method_call<'a, L: TemplateLanguage<'a>>( language: &L, build_ctx: &BuildContext<L::Property>, method: &MethodCallNode,) -> TemplateParseResult<Expression<L::Property>> { let mut expression = build_expression(language, build_ctx, &method.object)?; expression.property = language.build_method(build_ctx, expression.property, &method.function)?; expression.labels.push(method.function.name.to_owned()); Ok(expression)} pub fn build_core_method<'a, L: TemplateLanguage<'a>>( language: &L, build_ctx: &BuildContext<L::Property>, property: CoreTemplatePropertyKind<'a, L::Context>, function: &FunctionCallNode,) -> TemplateParseResult<L::Property> { match property { CoreTemplatePropertyKind::String(property) => { build_string_method(language, build_ctx, property, function) } CoreTemplatePropertyKind::StringList(property) => { build_formattable_list_method(language, build_ctx, property, function, |item| { language.wrap_string(item) }) } CoreTemplatePropertyKind::Boolean(property) => { build_boolean_method(language, build_ctx, property, function) } CoreTemplatePropertyKind::Integer(property) => { build_integer_method(language, build_ctx, property, function) } CoreTemplatePropertyKind::Signature(property) => { build_signature_method(language, build_ctx, property, function) } CoreTemplatePropertyKind::Timestamp(property) => { build_timestamp_method(language, build_ctx, property, function) } CoreTemplatePropertyKind::TimestampRange(property) => { build_timestamp_range_method(language, build_ctx, property, function) } CoreTemplatePropertyKind::Template(_) => { Err(TemplateParseError::no_such_method("Template", function)) } CoreTemplatePropertyKind::ListTemplate(template) => { build_list_template_method(language, build_ctx, template, function) } }} fn build_string_method<'a, L: TemplateLanguage<'a>>( language: &L, build_ctx: &BuildContext<L::Property>, self_property: impl TemplateProperty<L::Context, Output = String> + 'a, function: &FunctionCallNode,) -> TemplateParseResult<L::Property> { let property = match function.name { "contains" => { let [needle_node] = template_parser::expect_exact_arguments(function)?; // TODO: or .try_into_string() to disable implicit type cast? let needle_property = expect_plain_text_expression(language, build_ctx, needle_node)?; language.wrap_boolean(TemplateFunction::new( (self_property, needle_property), |(haystack, needle)| haystack.contains(&needle), )) } "starts_with" => { let [needle_node] = template_parser::expect_exact_arguments(function)?; let needle_property = expect_plain_text_expression(language, build_ctx, needle_node)?; language.wrap_boolean(TemplateFunction::new( (self_property, needle_property), move |(haystack, needle)| haystack.starts_with(&needle), )) } "ends_with" => { let [needle_node] = template_parser::expect_exact_arguments(function)?; let needle_property = expect_plain_text_expression(language, build_ctx, needle_node)?; language.wrap_boolean(TemplateFunction::new( (self_property, needle_property), move |(haystack, needle)| haystack.ends_with(&needle), )) } "remove_prefix" => { let [needle_node] = template_parser::expect_exact_arguments(function)?; let needle_property = expect_plain_text_expression(language, build_ctx, needle_node)?; language.wrap_string(TemplateFunction::new( (self_property, needle_property), move |(haystack, needle)| { haystack .strip_prefix(&needle) .map(ToOwned::to_owned) .unwrap_or(haystack) }, )) } "remove_suffix" => { let [needle_node] = template_parser::expect_exact_arguments(function)?; let needle_property = expect_plain_text_expression(language, build_ctx, needle_node)?; language.wrap_string(TemplateFunction::new( (self_property, needle_property), move |(haystack, needle)| { haystack .strip_suffix(&needle) .map(ToOwned::to_owned) .unwrap_or(haystack) }, )) } "substr" => { let [start_idx, end_idx] = template_parser::expect_exact_arguments(function)?; let start_idx_property = expect_integer_expression(language, build_ctx, start_idx)?; let end_idx_property = expect_integer_expression(language, build_ctx, end_idx)?; language.wrap_string(TemplateFunction::new( (self_property, start_idx_property, end_idx_property), |(s, start_idx, end_idx)| { // TODO: If we add .len() method, we'll expose bytes-based and char-based APIs. // Having different index units would be confusing, so we might want to change // .substr() to bytes-based and round up/down towards char or grapheme-cluster // boundary. let to_idx = |i: i64| -> usize { let magnitude = usize::try_from(i.unsigned_abs()).unwrap_or(usize::MAX); if i < 0 { s.chars().count().saturating_sub(magnitude) } else { magnitude } }; let start_idx = to_idx(start_idx); let end_idx = to_idx(end_idx); if start_idx >= end_idx { String::new() } else { s.chars() .skip(start_idx) .take(end_idx - start_idx) .collect() } }, )) } "first_line" => { template_parser::expect_no_arguments(function)?; language.wrap_string(TemplateFunction::new(self_property, |s| { s.lines().next().unwrap_or_default().to_string() })) } "lines" => { template_parser::expect_no_arguments(function)?; language.wrap_string_list(TemplateFunction::new(self_property, |s| { s.lines().map(|l| l.to_owned()).collect() })) } "upper" => { template_parser::expect_no_arguments(function)?; language.wrap_string(TemplateFunction::new(self_property, |s| s.to_uppercase())) } "lower" => { template_parser::expect_no_arguments(function)?; language.wrap_string(TemplateFunction::new(self_property, |s| s.to_lowercase())) } _ => return Err(TemplateParseError::no_such_method("String", function)), }; Ok(property)} fn build_boolean_method<'a, L: TemplateLanguage<'a>>( _language: &L, _build_ctx: &BuildContext<L::Property>, _self_property: impl TemplateProperty<L::Context, Output = bool> + 'a, function: &FunctionCallNode,) -> TemplateParseResult<L::Property> { Err(TemplateParseError::no_such_method("Boolean", function))} fn build_integer_method<'a, L: TemplateLanguage<'a>>( _language: &L, _build_ctx: &BuildContext<L::Property>, _self_property: impl TemplateProperty<L::Context, Output = i64> + 'a, function: &FunctionCallNode,) -> TemplateParseResult<L::Property> { Err(TemplateParseError::no_such_method("Integer", function))} fn build_signature_method<'a, L: TemplateLanguage<'a>>( language: &L, _build_ctx: &BuildContext<L::Property>, self_property: impl TemplateProperty<L::Context, Output = Signature> + 'a, function: &FunctionCallNode,) -> TemplateParseResult<L::Property> { let property = match function.name { "name" => { template_parser::expect_no_arguments(function)?; language.wrap_string(TemplateFunction::new(self_property, |signature| { signature.name })) } "email" => { template_parser::expect_no_arguments(function)?; language.wrap_string(TemplateFunction::new(self_property, |signature| { signature.email })) } "username" => { template_parser::expect_no_arguments(function)?; language.wrap_string(TemplateFunction::new(self_property, |signature| { let (username, _) = text_util::split_email(&signature.email); username.to_owned() })) } "timestamp" => { template_parser::expect_no_arguments(function)?; language.wrap_timestamp(TemplateFunction::new(self_property, |signature| { signature.timestamp })) } _ => return Err(TemplateParseError::no_such_method("Signature", function)), }; Ok(property)} fn build_timestamp_method<'a, L: TemplateLanguage<'a>>( language: &L, _build_ctx: &BuildContext<L::Property>, self_property: impl TemplateProperty<L::Context, Output = Timestamp> + 'a, function: &FunctionCallNode,Lines 1–500 of 1748. Load more
// 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::collections::HashMap; use itertools::Itertools as _; use jj_lib::backend::{Signature, Timestamp}; use crate::template_parser::{ self, ExpressionKind, ExpressionNode, FunctionCallNode, MethodCallNode, TemplateParseError, TemplateParseResult, }; use crate::templater::{ ConcatTemplate, ConditionalTemplate, IntoTemplate, LabelTemplate, ListPropertyTemplate, ListTemplate, Literal, PlainTextFormattedProperty, PropertyPlaceholder, ReformatTemplate, SeparateTemplate, Template, TemplateFunction, TemplateProperty, TimestampRange, }; use crate::{text_util, time_util}; /// Callbacks to build language-specific evaluation objects from AST nodes. pub trait TemplateLanguage<'a> { type Context: 'a; type Property: IntoTemplateProperty<'a, Self::Context>; fn wrap_string( &self, property: impl TemplateProperty<Self::Context, Output = String> + 'a, ) -> Self::Property; fn wrap_string_list( &self, property: impl TemplateProperty<Self::Context, Output = Vec<String>> + 'a, ) -> Self::Property; fn wrap_boolean( &self, property: impl TemplateProperty<Self::Context, Output = bool> + 'a, ) -> Self::Property; fn wrap_integer( &self, property: impl TemplateProperty<Self::Context, Output = i64> + 'a, ) -> Self::Property; fn wrap_signature( &self, property: impl TemplateProperty<Self::Context, Output = Signature> + 'a, ) -> Self::Property; fn wrap_timestamp( &self, property: impl TemplateProperty<Self::Context, Output = Timestamp> + 'a, ) -> Self::Property; fn wrap_timestamp_range( &self, property: impl TemplateProperty<Self::Context, Output = TimestampRange> + 'a, ) -> Self::Property; fn wrap_template(&self, template: Box<dyn Template<Self::Context> + 'a>) -> Self::Property; fn wrap_list_template( &self, template: Box<dyn ListTemplate<Self::Context> + 'a>, ) -> Self::Property; fn build_keyword(&self, name: &str, span: pest::Span) -> TemplateParseResult<Self::Property>; fn build_method( &self, build_ctx: &BuildContext<Self::Property>, property: Self::Property, function: &FunctionCallNode, ) -> TemplateParseResult<Self::Property>; } /// Implements `TemplateLanguage::wrap_<type>()` functions. /// /// - `impl_core_wrap_property_fns('a)` for `CoreTemplatePropertyKind`, /// - `impl_core_wrap_property_fns('a, MyKind::Core)` for `MyKind::Core(..)`. macro_rules! impl_core_wrap_property_fns { ($a:lifetime) => { $crate::template_builder::impl_core_wrap_property_fns!($a, std::convert::identity); }; ($a:lifetime, $outer:path) => { $crate::template_builder::impl_wrap_property_fns!( $a, $crate::template_builder::CoreTemplatePropertyKind, $outer, { wrap_string(String) => String, wrap_string_list(Vec<String>) => StringList, wrap_boolean(bool) => Boolean, wrap_integer(i64) => Integer, wrap_signature(jj_lib::backend::Signature) => Signature, wrap_timestamp(jj_lib::backend::Timestamp) => Timestamp, wrap_timestamp_range($crate::templater::TimestampRange) => TimestampRange, } ); fn wrap_template( &self, template: Box<dyn $crate::templater::Template<Self::Context> + $a>, ) -> Self::Property { use $crate::template_builder::CoreTemplatePropertyKind as Kind; $outer(Kind::Template(template)) } fn wrap_list_template( &self, template: Box<dyn $crate::templater::ListTemplate<Self::Context> + $a>, ) -> Self::Property { use $crate::template_builder::CoreTemplatePropertyKind as Kind; $outer(Kind::ListTemplate(template)) } }; } macro_rules! impl_wrap_property_fns { ($a:lifetime, $kind:path, $outer:path, { $( $func:ident($ty:ty) => $var:ident, )+ }) => { $( fn $func( &self, property: impl $crate::templater::TemplateProperty< Self::Context, Output = $ty> + $a, ) -> Self::Property { use $kind as Kind; // https://github.com/rust-lang/rust/issues/48067 $outer(Kind::$var(Box::new(property))) } )+ }; } pub(crate) use {impl_core_wrap_property_fns, impl_wrap_property_fns}; /// Provides access to basic template property types. pub trait IntoTemplateProperty<'a, C> { fn try_into_boolean(self) -> Option<Box<dyn TemplateProperty<C, Output = bool> + 'a>>; fn try_into_integer(self) -> Option<Box<dyn TemplateProperty<C, Output = i64> + 'a>>; fn try_into_plain_text(self) -> Option<Box<dyn TemplateProperty<C, Output = String> + 'a>>; fn try_into_template(self) -> Option<Box<dyn Template<C> + 'a>>; } pub enum CoreTemplatePropertyKind<'a, I> { String(Box<dyn TemplateProperty<I, Output = String> + 'a>), StringList(Box<dyn TemplateProperty<I, Output = Vec<String>> + 'a>), Boolean(Box<dyn TemplateProperty<I, Output = bool> + 'a>), Integer(Box<dyn TemplateProperty<I, Output = i64> + 'a>), Signature(Box<dyn TemplateProperty<I, Output = Signature> + 'a>), Timestamp(Box<dyn TemplateProperty<I, Output = Timestamp> + 'a>), TimestampRange(Box<dyn TemplateProperty<I, Output = TimestampRange> + 'a>), // Similar to `TemplateProperty<I, Output = Box<dyn Template<()> + 'a>`, but doesn't // capture `I` to produce `Template<()>`. The context `I` would have to be cloned // to convert `Template<I>` to `Template<()>`. Template(Box<dyn Template<I> + 'a>), ListTemplate(Box<dyn ListTemplate<I> + 'a>), } impl<'a, I: 'a> IntoTemplateProperty<'a, I> for CoreTemplatePropertyKind<'a, I> { fn try_into_boolean(self) -> Option<Box<dyn TemplateProperty<I, Output = bool> + 'a>> { match self { CoreTemplatePropertyKind::String(property) => { Some(Box::new(TemplateFunction::new(property, |s| !s.is_empty()))) } CoreTemplatePropertyKind::StringList(property) => { Some(Box::new(TemplateFunction::new(property, |l| !l.is_empty()))) } CoreTemplatePropertyKind::Boolean(property) => Some(property), CoreTemplatePropertyKind::Integer(_) => None, CoreTemplatePropertyKind::Signature(_) => None, CoreTemplatePropertyKind::Timestamp(_) => None, CoreTemplatePropertyKind::TimestampRange(_) => None, // Template types could also be evaluated to boolean, but it's less likely // to apply label() or .map() and use the result as conditional. It's also // unclear whether ListTemplate should behave as a "list" or a "template". CoreTemplatePropertyKind::Template(_) => None, CoreTemplatePropertyKind::ListTemplate(_) => None, } } fn try_into_integer(self) -> Option<Box<dyn TemplateProperty<I, Output = i64> + 'a>> { match self { CoreTemplatePropertyKind::Integer(property) => Some(property), _ => None, } } fn try_into_plain_text(self) -> Option<Box<dyn TemplateProperty<I, Output = String> + 'a>> { match self { CoreTemplatePropertyKind::String(property) => Some(property), _ => { let template = self.try_into_template()?; Some(Box::new(PlainTextFormattedProperty::new(template))) } } } fn try_into_template(self) -> Option<Box<dyn Template<I> + 'a>> { match self { CoreTemplatePropertyKind::String(property) => Some(property.into_template()), CoreTemplatePropertyKind::StringList(property) => Some(property.into_template()), CoreTemplatePropertyKind::Boolean(property) => Some(property.into_template()), CoreTemplatePropertyKind::Integer(property) => Some(property.into_template()), CoreTemplatePropertyKind::Signature(property) => Some(property.into_template()), CoreTemplatePropertyKind::Timestamp(property) => Some(property.into_template()), CoreTemplatePropertyKind::TimestampRange(property) => Some(property.into_template()), CoreTemplatePropertyKind::Template(template) => Some(template), CoreTemplatePropertyKind::ListTemplate(template) => Some(template.into_template()), } } } /// 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![]; Expression { property, labels } } fn with_label(property: P, label: impl Into<String>) -> Self { let labels = vec![label.into()]; Expression { property, labels } } pub fn try_into_boolean<'a, C: 'a>( self, ) -> Option<Box<dyn TemplateProperty<C, Output = bool> + 'a>> where P: IntoTemplateProperty<'a, C>, { self.property.try_into_boolean() } pub fn try_into_integer<'a, C: 'a>( self, ) -> Option<Box<dyn TemplateProperty<C, Output = i64> + 'a>> where P: IntoTemplateProperty<'a, C>, { self.property.try_into_integer() } pub fn try_into_plain_text<'a, C: 'a>( self, ) -> Option<Box<dyn TemplateProperty<C, Output = String> + 'a>> where P: IntoTemplateProperty<'a, C>, { self.property.try_into_plain_text() } pub fn try_into_template<'a, C: 'a>(self) -> Option<Box<dyn Template<C> + 'a>> where P: IntoTemplateProperty<'a, C>, { 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 struct BuildContext<'i, P> { /// Map of functions to create `L::Property`. local_variables: HashMap<&'i str, &'i (dyn Fn() -> P)>, } fn build_method_call<'a, L: TemplateLanguage<'a>>( language: &L, build_ctx: &BuildContext<L::Property>, method: &MethodCallNode, ) -> TemplateParseResult<Expression<L::Property>> { let mut expression = build_expression(language, build_ctx, &method.object)?; expression.property = language.build_method(build_ctx, expression.property, &method.function)?; expression.labels.push(method.function.name.to_owned()); Ok(expression) } pub fn build_core_method<'a, L: TemplateLanguage<'a>>( language: &L, build_ctx: &BuildContext<L::Property>, property: CoreTemplatePropertyKind<'a, L::Context>, function: &FunctionCallNode, ) -> TemplateParseResult<L::Property> { match property { CoreTemplatePropertyKind::String(property) => { build_string_method(language, build_ctx, property, function) } CoreTemplatePropertyKind::StringList(property) => { build_formattable_list_method(language, build_ctx, property, function, |item| { language.wrap_string(item) }) } CoreTemplatePropertyKind::Boolean(property) => { build_boolean_method(language, build_ctx, property, function) } CoreTemplatePropertyKind::Integer(property) => { build_integer_method(language, build_ctx, property, function) } CoreTemplatePropertyKind::Signature(property) => { build_signature_method(language, build_ctx, property, function) } CoreTemplatePropertyKind::Timestamp(property) => { build_timestamp_method(language, build_ctx, property, function) } CoreTemplatePropertyKind::TimestampRange(property) => { build_timestamp_range_method(language, build_ctx, property, function) } CoreTemplatePropertyKind::Template(_) => { Err(TemplateParseError::no_such_method("Template", function)) } CoreTemplatePropertyKind::ListTemplate(template) => { build_list_template_method(language, build_ctx, template, function) } } } fn build_string_method<'a, L: TemplateLanguage<'a>>( language: &L, build_ctx: &BuildContext<L::Property>, self_property: impl TemplateProperty<L::Context, Output = String> + 'a, function: &FunctionCallNode, ) -> TemplateParseResult<L::Property> { let property = match function.name { "contains" => { let [needle_node] = template_parser::expect_exact_arguments(function)?; // TODO: or .try_into_string() to disable implicit type cast? let needle_property = expect_plain_text_expression(language, build_ctx, needle_node)?; language.wrap_boolean(TemplateFunction::new( (self_property, needle_property), |(haystack, needle)| haystack.contains(&needle), )) } "starts_with" => { let [needle_node] = template_parser::expect_exact_arguments(function)?; let needle_property = expect_plain_text_expression(language, build_ctx, needle_node)?; language.wrap_boolean(TemplateFunction::new( (self_property, needle_property), move |(haystack, needle)| haystack.starts_with(&needle), )) } "ends_with" => { let [needle_node] = template_parser::expect_exact_arguments(function)?; let needle_property = expect_plain_text_expression(language, build_ctx, needle_node)?; language.wrap_boolean(TemplateFunction::new( (self_property, needle_property), move |(haystack, needle)| haystack.ends_with(&needle), )) } "remove_prefix" => { let [needle_node] = template_parser::expect_exact_arguments(function)?; let needle_property = expect_plain_text_expression(language, build_ctx, needle_node)?; language.wrap_string(TemplateFunction::new( (self_property, needle_property), move |(haystack, needle)| { haystack .strip_prefix(&needle) .map(ToOwned::to_owned) .unwrap_or(haystack) }, )) } "remove_suffix" => { let [needle_node] = template_parser::expect_exact_arguments(function)?; let needle_property = expect_plain_text_expression(language, build_ctx, needle_node)?; language.wrap_string(TemplateFunction::new( (self_property, needle_property), move |(haystack, needle)| { haystack .strip_suffix(&needle) .map(ToOwned::to_owned) .unwrap_or(haystack) }, )) } "substr" => { let [start_idx, end_idx] = template_parser::expect_exact_arguments(function)?; let start_idx_property = expect_integer_expression(language, build_ctx, start_idx)?; let end_idx_property = expect_integer_expression(language, build_ctx, end_idx)?; language.wrap_string(TemplateFunction::new( (self_property, start_idx_property, end_idx_property), |(s, start_idx, end_idx)| { // TODO: If we add .len() method, we'll expose bytes-based and char-based APIs. // Having different index units would be confusing, so we might want to change // .substr() to bytes-based and round up/down towards char or grapheme-cluster // boundary. let to_idx = |i: i64| -> usize { let magnitude = usize::try_from(i.unsigned_abs()).unwrap_or(usize::MAX); if i < 0 { s.chars().count().saturating_sub(magnitude) } else { magnitude } }; let start_idx = to_idx(start_idx); let end_idx = to_idx(end_idx); if start_idx >= end_idx { String::new() } else { s.chars() .skip(start_idx) .take(end_idx - start_idx) .collect() } }, )) } "first_line" => { template_parser::expect_no_arguments(function)?; language.wrap_string(TemplateFunction::new(self_property, |s| { s.lines().next().unwrap_or_default().to_string() })) } "lines" => { template_parser::expect_no_arguments(function)?; language.wrap_string_list(TemplateFunction::new(self_property, |s| { s.lines().map(|l| l.to_owned()).collect() })) } "upper" => { template_parser::expect_no_arguments(function)?; language.wrap_string(TemplateFunction::new(self_property, |s| s.to_uppercase())) } "lower" => { template_parser::expect_no_arguments(function)?; language.wrap_string(TemplateFunction::new(self_property, |s| s.to_lowercase())) } _ => return Err(TemplateParseError::no_such_method("String", function)), }; Ok(property) } fn build_boolean_method<'a, L: TemplateLanguage<'a>>( _language: &L, _build_ctx: &BuildContext<L::Property>, _self_property: impl TemplateProperty<L::Context, Output = bool> + 'a, function: &FunctionCallNode, ) -> TemplateParseResult<L::Property> { Err(TemplateParseError::no_such_method("Boolean", function)) } fn build_integer_method<'a, L: TemplateLanguage<'a>>( _language: &L, _build_ctx: &BuildContext<L::Property>, _self_property: impl TemplateProperty<L::Context, Output = i64> + 'a, function: &FunctionCallNode, ) -> TemplateParseResult<L::Property> { Err(TemplateParseError::no_such_method("Integer", function)) } fn build_signature_method<'a, L: TemplateLanguage<'a>>( language: &L, _build_ctx: &BuildContext<L::Property>, self_property: impl TemplateProperty<L::Context, Output = Signature> + 'a, function: &FunctionCallNode, ) -> TemplateParseResult<L::Property> { let property = match function.name { "name" => { template_parser::expect_no_arguments(function)?; language.wrap_string(TemplateFunction::new(self_property, |signature| { signature.name })) } "email" => { template_parser::expect_no_arguments(function)?; language.wrap_string(TemplateFunction::new(self_property, |signature| { signature.email })) } "username" => { template_parser::expect_no_arguments(function)?; language.wrap_string(TemplateFunction::new(self_property, |signature| { let (username, _) = text_util::split_email(&signature.email); username.to_owned() })) } "timestamp" => { template_parser::expect_no_arguments(function)?; language.wrap_timestamp(TemplateFunction::new(self_property, |signature| { signature.timestamp })) } _ => return Err(TemplateParseError::no_such_method("Signature", function)), }; Ok(property) } fn build_timestamp_method<'a, L: TemplateLanguage<'a>>( language: &L, _build_ctx: &BuildContext<L::Property>, self_property: impl TemplateProperty<L::Context, Output = Timestamp> + 'a, function: &FunctionCallNode,