diff --git a/cli/src/commit_templater.rs b/cli/src/commit_templater.rs index 50e28ecc8f..dbd037b51a 100644 --- a/cli/src/commit_templater.rs +++ b/cli/src/commit_templater.rs @@ -54,8 +54,9 @@ impl<'repo> TemplateLanguage<'repo> for CommitTemplateLanguage<'repo, '_> { template_builder::impl_core_wrap_property_fns!('repo, CommitTemplatePropertyKind::Core); - fn build_keyword(&self, name: &str, span: pest::Span) -> TemplateParseResult { - build_commit_keyword(self, name, span) + fn build_self(&self) -> Self::Property { + // Commit object is lightweight (a few Arc + CommitId) + self.wrap_commit(TemplatePropertyFn(|commit: &Commit| commit.clone())) } fn build_method( @@ -236,21 +237,6 @@ impl CommitKeywordCache { } } -fn build_commit_keyword<'repo>( - language: &CommitTemplateLanguage<'repo, '_>, - name: &str, - span: pest::Span, -) -> TemplateParseResult> { - // Commit object is lightweight (a few Arc + CommitId), so just clone it - // to turn into a property type. Abstraction over "for<'a> (&'a T) -> &'a T" - // and "(&T) -> T" wouldn't be simple. If we want to remove Clone/Rc/Arc, - // maybe we can add an abstraction that takes "Fn(&Commit) -> O" and returns - // "TemplateProperty". - let property = TemplatePropertyFn(|commit: &Commit| commit.clone()); - build_commit_keyword_opt(language, property, name) - .ok_or_else(|| TemplateParseError::no_such_keyword(name, span)) -} - fn build_commit_method<'repo>( language: &CommitTemplateLanguage<'repo, '_>, _build_ctx: &BuildContext>, @@ -265,6 +251,7 @@ fn build_commit_method<'repo>( } } +// TODO: merge into build_commit_method() fn build_commit_keyword_opt<'repo>( language: &CommitTemplateLanguage<'repo, '_>, property: impl TemplateProperty + 'repo, diff --git a/cli/src/operation_templater.rs b/cli/src/operation_templater.rs index 0b1384f155..0d3981cee1 100644 --- a/cli/src/operation_templater.rs +++ b/cli/src/operation_templater.rs @@ -42,8 +42,9 @@ impl TemplateLanguage<'static> for OperationTemplateLanguage<'_> { template_builder::impl_core_wrap_property_fns!('static, OperationTemplatePropertyKind::Core); - fn build_keyword(&self, name: &str, span: pest::Span) -> TemplateParseResult { - build_operation_keyword(self, name, span) + fn build_self(&self) -> Self::Property { + // Operation object is lightweight (a few Arc + OperationId) + self.wrap_operation(TemplatePropertyFn(|op: &Operation| op.clone())) } fn build_method( @@ -56,6 +57,9 @@ impl TemplateLanguage<'static> for OperationTemplateLanguage<'_> { OperationTemplatePropertyKind::Core(property) => { template_builder::build_core_method(self, build_ctx, property, function) } + OperationTemplatePropertyKind::Operation(property) => { + build_operation_method(self, build_ctx, property, function) + } OperationTemplatePropertyKind::OperationId(property) => { build_operation_id_method(self, build_ctx, property, function) } @@ -64,6 +68,13 @@ impl TemplateLanguage<'static> for OperationTemplateLanguage<'_> { } impl OperationTemplateLanguage<'_> { + fn wrap_operation( + &self, + property: impl TemplateProperty + 'static, + ) -> OperationTemplatePropertyKind { + OperationTemplatePropertyKind::Operation(Box::new(property)) + } + fn wrap_operation_id( &self, property: impl TemplateProperty + 'static, @@ -74,6 +85,7 @@ impl OperationTemplateLanguage<'_> { enum OperationTemplatePropertyKind { Core(CoreTemplatePropertyKind<'static, Operation>), + Operation(Box>), OperationId(Box>), } @@ -81,6 +93,7 @@ impl IntoTemplateProperty<'static, Operation> for OperationTemplatePropertyKind fn try_into_boolean(self) -> Option>> { match self { OperationTemplatePropertyKind::Core(property) => property.try_into_boolean(), + OperationTemplatePropertyKind::Operation(_) => None, OperationTemplatePropertyKind::OperationId(_) => None, } } @@ -105,35 +118,57 @@ impl IntoTemplateProperty<'static, Operation> for OperationTemplatePropertyKind fn try_into_template(self) -> Option>> { match self { OperationTemplatePropertyKind::Core(property) => property.try_into_template(), + OperationTemplatePropertyKind::Operation(_) => None, OperationTemplatePropertyKind::OperationId(property) => Some(property.into_template()), } } } -fn build_operation_keyword( +fn build_operation_method( language: &OperationTemplateLanguage, - name: &str, - span: pest::Span, + _build_ctx: &BuildContext, + self_property: impl TemplateProperty + 'static, + function: &FunctionCallNode, ) -> TemplateParseResult { - fn wrap_fn O>(f: F) -> TemplatePropertyFn { - TemplatePropertyFn(f) + if let Some(property) = build_operation_keyword_opt(language, self_property, function.name) { + template_parser::expect_no_arguments(function)?; + Ok(property) + } else { + Err(TemplateParseError::no_such_method("Operation", function)) + } +} + +// TODO: merge into build_operation_method() +fn build_operation_keyword_opt( + language: &OperationTemplateLanguage, + property: impl TemplateProperty + 'static, + name: &str, +) -> Option { + fn wrap_fn( + property: impl TemplateProperty, + f: impl Fn(&Operation) -> O, + ) -> impl TemplateProperty { + TemplateFunction::new(property, move |op| f(&op)) } fn wrap_metadata_fn( - f: impl Fn(&OperationMetadata) -> O + 'static, + property: impl TemplateProperty, + f: impl Fn(&OperationMetadata) -> O, ) -> impl TemplateProperty { - wrap_fn(move |op| f(&op.store_operation().metadata)) + TemplateFunction::new(property, move |op| f(&op.store_operation().metadata)) } let property = match name { "current_operation" => { let current_op_id = language.current_op_id.cloned(); - language.wrap_boolean(wrap_fn(move |op| Some(op.id()) == current_op_id.as_ref())) - } - "description" => { - language.wrap_string(wrap_metadata_fn(|metadata| metadata.description.clone())) + language.wrap_boolean(wrap_fn(property, move |op| { + Some(op.id()) == current_op_id.as_ref() + })) } - "id" => language.wrap_operation_id(wrap_fn(|op| op.id().clone())), - "tags" => language.wrap_string(wrap_metadata_fn(|metadata| { + "description" => language.wrap_string(wrap_metadata_fn(property, |metadata| { + metadata.description.clone() + })), + "id" => language.wrap_operation_id(wrap_fn(property, |op| op.id().clone())), + "tags" => language.wrap_string(wrap_metadata_fn(property, |metadata| { // TODO: introduce map type metadata .tags @@ -141,21 +176,23 @@ fn build_operation_keyword( .map(|(key, value)| format!("{key}: {value}")) .join("\n") })), - "time" => language.wrap_timestamp_range(wrap_metadata_fn(|metadata| TimestampRange { - start: metadata.start_time.clone(), - end: metadata.end_time.clone(), - })), - "user" => language.wrap_string(wrap_metadata_fn(|metadata| { + "time" => { + language.wrap_timestamp_range(wrap_metadata_fn(property, |metadata| TimestampRange { + start: metadata.start_time.clone(), + end: metadata.end_time.clone(), + })) + } + "user" => language.wrap_string(wrap_metadata_fn(property, |metadata| { // TODO: introduce dedicated type and provide accessors? format!("{}@{}", metadata.username, metadata.hostname) })), "root" => { let root_op_id = language.root_op_id.clone(); - language.wrap_boolean(wrap_fn(move |op| op.id() == &root_op_id)) + language.wrap_boolean(wrap_fn(property, move |op| op.id() == &root_op_id)) } - _ => return Err(TemplateParseError::no_such_keyword(name, span)), + _ => return None, }; - Ok(property) + Some(property) } impl Template<()> for OperationId { diff --git a/cli/src/template_builder.rs b/cli/src/template_builder.rs index 13933158b6..8bf8d46339 100644 --- a/cli/src/template_builder.rs +++ b/cli/src/template_builder.rs @@ -68,7 +68,10 @@ pub trait TemplateLanguage<'a> { template: Box + 'a>, ) -> Self::Property; - fn build_keyword(&self, name: &str, span: pest::Span) -> TemplateParseResult; + /// 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, @@ -272,6 +275,28 @@ pub struct BuildContext<'i, P> { local_variables: HashMap<&'i str, &'i (dyn Fn() -> P)>, } +fn build_keyword<'a, L: TemplateLanguage<'a>>( + language: &L, + build_ctx: &BuildContext, + name: &str, + name_span: pest::Span<'_>, +) -> TemplateParseResult> { + // 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()), + }; + let property = language + .build_method(build_ctx, self_property, &function) + // Since keyword is a 0-ary method, any argument-related errors mean + // there's no such keyword. + .map_err(|_| TemplateParseError::no_such_keyword(name, name_span))?; + Ok(Expression::with_label(property, name)) +} + fn build_unary_operation<'a, L: TemplateLanguage<'a>>( language: &L, build_ctx: &BuildContext, @@ -842,8 +867,7 @@ pub fn build_expression<'a, L: TemplateLanguage<'a>>( // Don't label a local variable with its name Ok(Expression::unlabeled(make())) } else { - let property = language.build_keyword(name, node.span)?; - Ok(Expression::with_label(property, *name)) + build_keyword(language, build_ctx, name, node.span) } } ExpressionKind::Boolean(value) => { @@ -956,19 +980,12 @@ mod tests { impl TemplateLanguage<'static> for TestTemplateLanguage { type Context = (); - type Property = CoreTemplatePropertyKind<'static, ()>; + type Property = TestTemplatePropertyKind; - impl_core_wrap_property_fns!('static); + impl_core_wrap_property_fns!('static, TestTemplatePropertyKind::Core); - fn build_keyword( - &self, - name: &str, - span: pest::Span, - ) -> TemplateParseResult { - self.keywords - .get(name) - .map(|f| f(self)) - .ok_or_else(|| TemplateParseError::no_such_keyword(name, span)) + fn build_self(&self) -> Self::Property { + TestTemplatePropertyKind::Unit } fn build_method( @@ -977,11 +994,58 @@ mod tests { property: Self::Property, function: &FunctionCallNode, ) -> TemplateParseResult { - build_core_method(self, build_ctx, property, function) + match property { + TestTemplatePropertyKind::Core(property) => { + build_core_method(self, build_ctx, property, function) + } + TestTemplatePropertyKind::Unit => { + let build = self + .keywords + .get(function.name) + .ok_or_else(|| TemplateParseError::no_such_method("()", function))?; + template_parser::expect_no_arguments(function)?; + Ok(build(self)) + } + } + } + } + + enum TestTemplatePropertyKind { + Core(CoreTemplatePropertyKind<'static, ()>), + Unit, + } + + impl IntoTemplateProperty<'static, ()> for TestTemplatePropertyKind { + fn try_into_boolean(self) -> Option>> { + match self { + TestTemplatePropertyKind::Core(property) => property.try_into_boolean(), + TestTemplatePropertyKind::Unit => None, + } + } + + fn try_into_integer(self) -> Option>> { + match self { + TestTemplatePropertyKind::Core(property) => property.try_into_integer(), + TestTemplatePropertyKind::Unit => None, + } + } + + fn try_into_plain_text(self) -> Option>> { + match self { + TestTemplatePropertyKind::Core(property) => property.try_into_plain_text(), + TestTemplatePropertyKind::Unit => None, + } + } + + fn try_into_template(self) -> Option>> { + match self { + TestTemplatePropertyKind::Core(property) => property.try_into_template(), + TestTemplatePropertyKind::Unit => None, + } } } - type TestTemplateKeywordFn = fn(&TestTemplateLanguage) -> CoreTemplatePropertyKind<'static, ()>; + type TestTemplateKeywordFn = fn(&TestTemplateLanguage) -> TestTemplatePropertyKind; /// Helper to set up template evaluation environment. #[derive(Clone, Default)] diff --git a/docs/templates.md b/docs/templates.md index cbeaec3aeb..dc37115f9d 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -120,6 +120,11 @@ The following methods are defined. See also the `List` type. * `.join(separator: Template) -> Template` +### Operation type + +This type cannot be printed. All operation keywords are accessible as 0-argument +methods. + ### OperationId type The following methods are defined. diff --git a/lib/src/operation.rs b/lib/src/operation.rs index 896ab8ce23..8f09e81423 100644 --- a/lib/src/operation.rs +++ b/lib/src/operation.rs @@ -27,7 +27,7 @@ use crate::view::View; pub struct Operation { op_store: Arc, id: OperationId, - data: op_store::Operation, + data: Arc, // allow cheap clone } impl Debug for Operation { @@ -63,8 +63,16 @@ impl Hash for Operation { } impl Operation { - pub fn new(op_store: Arc, id: OperationId, data: op_store::Operation) -> Self { - Operation { op_store, id, data } + pub fn new( + op_store: Arc, + id: OperationId, + data: impl Into>, + ) -> Self { + Operation { + op_store, + id, + data: data.into(), + } } pub fn op_store(&self) -> Arc {