// 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, BinaryOp, ExpressionKind, ExpressionNode, FunctionCallNode, MethodCallNode, TemplateAliasesMap, TemplateParseError, TemplateParseErrorKind, TemplateParseResult, UnaryOp,};use crate::templater::{ ConcatTemplate, ConditionalTemplate, IntoTemplate, LabelTemplate, ListPropertyTemplate, ListTemplate, Literal, PlainTextFormattedProperty, PropertyPlaceholder, ReformatTemplate, SeparateTemplate, Template, TemplateFunction, TemplateProperty, TemplatePropertyError, 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; /// Creates the `self` template property, which is usually a function that /// clones the `Context` object. fn build_self(&self) -> 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| { Ok(!s.is_empty()) }))) } CoreTemplatePropertyKind::StringList(property) => { Some(Box::new(TemplateFunction::new(property, |l| { Ok(!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), _ => { // TODO: Runtime property evaluation error will be materialized // as string here. Ideally, the error should propagate because // the caller expects a value, not a template to render. Some // ideas to work around the problem: // 1. stringify property without using Template type (works only for // non-template expressions) // 2. add flag to propagate property error as io::Error::other() (e.g. // Template::format(context, formatter, propagate_err)) 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()), } }} /// Function that translates method call node of self type `T`.// 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 TemplateBuildMethodFn<'a, L, T> = fn( &L, &BuildContext<<L as TemplateLanguage<'a>>::Property>, Box<dyn TemplateProperty<<L as TemplateLanguage<'a>>::Context, Output = T> + 'a>, &FunctionCallNode, ) -> TemplateParseResult<<L as TemplateLanguage<'a>>::Property>; /// Table of functions that translate method call node of self type `T`.pub type TemplateBuildMethodFnMap<'a, L, T> = HashMap<&'static str, TemplateBuildMethodFn<'a, L, T>>; /// Symbol table of methods available in the core template.pub struct CoreTemplateBuildFnTable<'a, L: TemplateLanguage<'a> + ?Sized> { pub string_methods: TemplateBuildMethodFnMap<'a, L, String>, pub boolean_methods: TemplateBuildMethodFnMap<'a, L, bool>, pub integer_methods: TemplateBuildMethodFnMap<'a, L, i64>, pub signature_methods: TemplateBuildMethodFnMap<'a, L, Signature>, pub timestamp_methods: TemplateBuildMethodFnMap<'a, L, Timestamp>, pub timestamp_range_methods: TemplateBuildMethodFnMap<'a, L, TimestampRange>, // TODO: add global functions table?} pub fn merge_fn_map<'a, L: TemplateLanguage<'a> + ?Sized, T>( base: &mut TemplateBuildMethodFnMap<'a, L, T>, extension: TemplateBuildMethodFnMap<'a, L, T>,) { for (name, function) in extension { if base.insert(name, function).is_some() { panic!("Conflicting template definitions for '{name}' function"); } }} impl<'a, L: TemplateLanguage<'a> + ?Sized> CoreTemplateBuildFnTable<'a, L> { /// Creates new symbol table containing the builtin methods. pub fn builtin() -> Self { CoreTemplateBuildFnTable { string_methods: builtin_string_methods(), boolean_methods: HashMap::new(), integer_methods: HashMap::new(), signature_methods: builtin_signature_methods(), timestamp_methods: builtin_timestamp_methods(), timestamp_range_methods: builtin_timestamp_range_methods(), } } pub fn empty() -> Self { CoreTemplateBuildFnTable { string_methods: HashMap::new(), boolean_methods: HashMap::new(), integer_methods: HashMap::new(), signature_methods: HashMap::new(), timestamp_methods: HashMap::new(), timestamp_range_methods: HashMap::new(), } } pub fn merge(&mut self, extension: CoreTemplateBuildFnTable<'a, L>) { let CoreTemplateBuildFnTable { string_methods, boolean_methods, integer_methods, signature_methods, timestamp_methods, timestamp_range_methods, } = extension; merge_fn_map(&mut self.string_methods, string_methods); merge_fn_map(&mut self.boolean_methods, boolean_methods); merge_fn_map(&mut self.integer_methods, integer_methods); merge_fn_map(&mut self.signature_methods, signature_methods); merge_fn_map(&mut self.timestamp_methods, timestamp_methods); merge_fn_map(&mut self.timestamp_range_methods, timestamp_range_methods); } /// Applies the method call node `function` to the given `property` by using /// this symbol table. pub fn build_method( &self, language: &L, build_ctx: &BuildContext<L::Property>, property: CoreTemplatePropertyKind<'a, L::Context>, function: &FunctionCallNode, ) -> TemplateParseResult<L::Property> { match property { CoreTemplatePropertyKind::String(property) => { let table = &self.string_methods; let build = template_parser::lookup_method("String", table, function)?; build(language, build_ctx, property, function) } CoreTemplatePropertyKind::StringList(property) => { // TODO: migrate to table? build_formattable_list_method(language, build_ctx, property, function, |item| { language.wrap_string(item) }) } CoreTemplatePropertyKind::Boolean(property) => { let table = &self.boolean_methods; let build = template_parser::lookup_method("Boolean", table, function)?; build(language, build_ctx, property, function) } CoreTemplatePropertyKind::Integer(property) => { let table = &self.integer_methods; let build = template_parser::lookup_method("Integer", table, function)?; build(language, build_ctx, property, function) } CoreTemplatePropertyKind::Signature(property) => { let table = &self.signature_methods; let build = template_parser::lookup_method("Signature", table, function)?; build(language, build_ctx, property, function) } CoreTemplatePropertyKind::Timestamp(property) => { let table = &self.timestamp_methods; let build = template_parser::lookup_method("Timestamp", table, function)?; build(language, build_ctx, property, function) } CoreTemplatePropertyKind::TimestampRange(property) => { let table = &self.timestamp_range_methods; let build = template_parser::lookup_method("TimestampRange", table, function)?; build(language, build_ctx, property, function) } CoreTemplatePropertyKind::Template(_) => { // TODO: migrate to table? Err(TemplateParseError::no_such_method("Template", function)) } CoreTemplatePropertyKind::ListTemplate(template) => { // TODO: migrate to table? build_list_template_method(language, build_ctx, template, 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![]; 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_keyword<'a, L: TemplateLanguage<'a> + ?Sized>( language: &L, build_ctx: &BuildContext<L::Property>, name: &str, name_span: pest::Span<'_>,) -> TemplateParseResult<Expression<L::Property>> { // Keyword is a 0-ary method on the "self" property let self_property = language.build_self(); let function = FunctionCallNode { name, name_span, args: vec![], args_span: name_span.end_pos().span(&name_span.end_pos()), }; match language.build_method(build_ctx, self_property, &function) { Ok(property) => Ok(Expression::with_label(property, name)), Err(err) => { let kind = if let TemplateParseErrorKind::NoSuchMethod { candidates, .. } = err.kind() { TemplateParseErrorKind::NoSuchKeyword { name: name.to_owned(), // TODO: filter methods by arity? candidates: candidates.clone(), } } else { // Since keyword is a 0-ary method, any argument-related errors // mean there's no such keyword. TemplateParseErrorKind::NoSuchKeyword { name: name.to_owned(), // TODO: might be better to phrase the error differently candidates: vec![format!("self.{name}(..)")], } }; Err(TemplateParseError::with_span(kind, name_span)) } }} fn build_unary_operation<'a, L: TemplateLanguage<'a> + ?Sized>( language: &L, build_ctx: &BuildContext<L::Property>, op: UnaryOp, arg_node: &ExpressionNode,) -> TemplateParseResult<Expression<L::Property>> { let property = match op { UnaryOp::LogicalNot => { let arg = expect_boolean_expression(language, build_ctx, arg_node)?; language.wrap_boolean(TemplateFunction::new(arg, |v| Ok(!v))) } UnaryOp::Negate => { let arg = expect_integer_expression(language, build_ctx, arg_node)?; language.wrap_integer(TemplateFunction::new(arg, |v| { v.checked_neg() .ok_or_else(|| TemplatePropertyError("Attempt to negate with overflow".into())) })) } }; Ok(Expression::unlabeled(property))} fn build_binary_operation<'a, L: TemplateLanguage<'a> + ?Sized>( language: &L, build_ctx: &BuildContext<L::Property>, op: BinaryOp, lhs_node: &ExpressionNode, rhs_node: &ExpressionNode,) -> TemplateParseResult<Expression<L::Property>> { let property = match op { BinaryOp::LogicalOr => { // No short-circuiting supported let lhs = expect_boolean_expression(language, build_ctx, lhs_node)?;Lines 1–500 of 2127. 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, BinaryOp, ExpressionKind, ExpressionNode, FunctionCallNode, MethodCallNode, TemplateAliasesMap, TemplateParseError, TemplateParseErrorKind, TemplateParseResult, UnaryOp, }; use crate::templater::{ ConcatTemplate, ConditionalTemplate, IntoTemplate, LabelTemplate, ListPropertyTemplate, ListTemplate, Literal, PlainTextFormattedProperty, PropertyPlaceholder, ReformatTemplate, SeparateTemplate, Template, TemplateFunction, TemplateProperty, TemplatePropertyError, 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; /// Creates the `self` template property, which is usually a function that /// clones the `Context` object. fn build_self(&self) -> 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| { Ok(!s.is_empty()) }))) } CoreTemplatePropertyKind::StringList(property) => { Some(Box::new(TemplateFunction::new(property, |l| { Ok(!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), _ => { // TODO: Runtime property evaluation error will be materialized // as string here. Ideally, the error should propagate because // the caller expects a value, not a template to render. Some // ideas to work around the problem: // 1. stringify property without using Template type (works only for // non-template expressions) // 2. add flag to propagate property error as io::Error::other() (e.g. // Template::format(context, formatter, propagate_err)) 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()), } } } /// Function that translates method call node of self type `T`. // 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 TemplateBuildMethodFn<'a, L, T> = fn( &L, &BuildContext<<L as TemplateLanguage<'a>>::Property>, Box<dyn TemplateProperty<<L as TemplateLanguage<'a>>::Context, Output = T> + 'a>, &FunctionCallNode, ) -> TemplateParseResult<<L as TemplateLanguage<'a>>::Property>; /// Table of functions that translate method call node of self type `T`. pub type TemplateBuildMethodFnMap<'a, L, T> = HashMap<&'static str, TemplateBuildMethodFn<'a, L, T>>; /// Symbol table of methods available in the core template. pub struct CoreTemplateBuildFnTable<'a, L: TemplateLanguage<'a> + ?Sized> { pub string_methods: TemplateBuildMethodFnMap<'a, L, String>, pub boolean_methods: TemplateBuildMethodFnMap<'a, L, bool>, pub integer_methods: TemplateBuildMethodFnMap<'a, L, i64>, pub signature_methods: TemplateBuildMethodFnMap<'a, L, Signature>, pub timestamp_methods: TemplateBuildMethodFnMap<'a, L, Timestamp>, pub timestamp_range_methods: TemplateBuildMethodFnMap<'a, L, TimestampRange>, // TODO: add global functions table? } pub fn merge_fn_map<'a, L: TemplateLanguage<'a> + ?Sized, T>( base: &mut TemplateBuildMethodFnMap<'a, L, T>, extension: TemplateBuildMethodFnMap<'a, L, T>, ) { for (name, function) in extension { if base.insert(name, function).is_some() { panic!("Conflicting template definitions for '{name}' function"); } } } impl<'a, L: TemplateLanguage<'a> + ?Sized> CoreTemplateBuildFnTable<'a, L> { /// Creates new symbol table containing the builtin methods. pub fn builtin() -> Self { CoreTemplateBuildFnTable { string_methods: builtin_string_methods(), boolean_methods: HashMap::new(), integer_methods: HashMap::new(), signature_methods: builtin_signature_methods(), timestamp_methods: builtin_timestamp_methods(), timestamp_range_methods: builtin_timestamp_range_methods(), } } pub fn empty() -> Self { CoreTemplateBuildFnTable { string_methods: HashMap::new(), boolean_methods: HashMap::new(), integer_methods: HashMap::new(), signature_methods: HashMap::new(), timestamp_methods: HashMap::new(), timestamp_range_methods: HashMap::new(), } } pub fn merge(&mut self, extension: CoreTemplateBuildFnTable<'a, L>) { let CoreTemplateBuildFnTable { string_methods, boolean_methods, integer_methods, signature_methods, timestamp_methods, timestamp_range_methods, } = extension; merge_fn_map(&mut self.string_methods, string_methods); merge_fn_map(&mut self.boolean_methods, boolean_methods); merge_fn_map(&mut self.integer_methods, integer_methods); merge_fn_map(&mut self.signature_methods, signature_methods); merge_fn_map(&mut self.timestamp_methods, timestamp_methods); merge_fn_map(&mut self.timestamp_range_methods, timestamp_range_methods); } /// Applies the method call node `function` to the given `property` by using /// this symbol table. pub fn build_method( &self, language: &L, build_ctx: &BuildContext<L::Property>, property: CoreTemplatePropertyKind<'a, L::Context>, function: &FunctionCallNode, ) -> TemplateParseResult<L::Property> { match property { CoreTemplatePropertyKind::String(property) => { let table = &self.string_methods; let build = template_parser::lookup_method("String", table, function)?; build(language, build_ctx, property, function) } CoreTemplatePropertyKind::StringList(property) => { // TODO: migrate to table? build_formattable_list_method(language, build_ctx, property, function, |item| { language.wrap_string(item) }) } CoreTemplatePropertyKind::Boolean(property) => { let table = &self.boolean_methods; let build = template_parser::lookup_method("Boolean", table, function)?; build(language, build_ctx, property, function) } CoreTemplatePropertyKind::Integer(property) => { let table = &self.integer_methods; let build = template_parser::lookup_method("Integer", table, function)?; build(language, build_ctx, property, function) } CoreTemplatePropertyKind::Signature(property) => { let table = &self.signature_methods; let build = template_parser::lookup_method("Signature", table, function)?; build(language, build_ctx, property, function) } CoreTemplatePropertyKind::Timestamp(property) => { let table = &self.timestamp_methods; let build = template_parser::lookup_method("Timestamp", table, function)?; build(language, build_ctx, property, function) } CoreTemplatePropertyKind::TimestampRange(property) => { let table = &self.timestamp_range_methods; let build = template_parser::lookup_method("TimestampRange", table, function)?; build(language, build_ctx, property, function) } CoreTemplatePropertyKind::Template(_) => { // TODO: migrate to table? Err(TemplateParseError::no_such_method("Template", function)) } CoreTemplatePropertyKind::ListTemplate(template) => { // TODO: migrate to table? build_list_template_method(language, build_ctx, template, 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![]; 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_keyword<'a, L: TemplateLanguage<'a> + ?Sized>( language: &L, build_ctx: &BuildContext<L::Property>, name: &str, name_span: pest::Span<'_>, ) -> TemplateParseResult<Expression<L::Property>> { // Keyword is a 0-ary method on the "self" property let self_property = language.build_self(); let function = FunctionCallNode { name, name_span, args: vec![], args_span: name_span.end_pos().span(&name_span.end_pos()), }; match language.build_method(build_ctx, self_property, &function) { Ok(property) => Ok(Expression::with_label(property, name)), Err(err) => { let kind = if let TemplateParseErrorKind::NoSuchMethod { candidates, .. } = err.kind() { TemplateParseErrorKind::NoSuchKeyword { name: name.to_owned(), // TODO: filter methods by arity? candidates: candidates.clone(), } } else { // Since keyword is a 0-ary method, any argument-related errors // mean there's no such keyword. TemplateParseErrorKind::NoSuchKeyword { name: name.to_owned(), // TODO: might be better to phrase the error differently candidates: vec![format!("self.{name}(..)")], } }; Err(TemplateParseError::with_span(kind, name_span)) } } } fn build_unary_operation<'a, L: TemplateLanguage<'a> + ?Sized>( language: &L, build_ctx: &BuildContext<L::Property>, op: UnaryOp, arg_node: &ExpressionNode, ) -> TemplateParseResult<Expression<L::Property>> { let property = match op { UnaryOp::LogicalNot => { let arg = expect_boolean_expression(language, build_ctx, arg_node)?; language.wrap_boolean(TemplateFunction::new(arg, |v| Ok(!v))) } UnaryOp::Negate => { let arg = expect_integer_expression(language, build_ctx, arg_node)?; language.wrap_integer(TemplateFunction::new(arg, |v| { v.checked_neg() .ok_or_else(|| TemplatePropertyError("Attempt to negate with overflow".into())) })) } }; Ok(Expression::unlabeled(property)) } fn build_binary_operation<'a, L: TemplateLanguage<'a> + ?Sized>( language: &L, build_ctx: &BuildContext<L::Property>, op: BinaryOp, lhs_node: &ExpressionNode, rhs_node: &ExpressionNode, ) -> TemplateParseResult<Expression<L::Property>> { let property = match op { BinaryOp::LogicalOr => { // No short-circuiting supported let lhs = expect_boolean_expression(language, build_ctx, lhs_node)?;