From 11bef2a1b8146fb4adae4fcd66bd05b1386adc73 Mon Sep 17 00:00:00 2001 From: Yuya Nishihara Date: Thu, 21 Mar 2024 17:12:31 +0900 Subject: [PATCH 1/4] templater: remove IntoTemplate abstraction, use extension method instead This is a remainder of the previous refactoring series. into_template() could be implemented as a non-extension method, which allows us to get rid of .clone() from Literal property extraction. However, there wasn't measurable difference. Let's not try to overly optimize things. It's probably simpler to switch to Rc if .clone() really matters. --- cli/src/commit_templater.rs | 2 +- cli/src/operation_templater.rs | 2 +- cli/src/template_builder.rs | 8 ++++---- cli/src/templater.rs | 22 +++++++++------------- 4 files changed, 15 insertions(+), 19 deletions(-) diff --git a/cli/src/commit_templater.rs b/cli/src/commit_templater.rs index a341df4214..46d203dc06 100644 --- a/cli/src/commit_templater.rs +++ b/cli/src/commit_templater.rs @@ -37,7 +37,7 @@ use crate::template_builder::{ }; use crate::template_parser::{self, FunctionCallNode, TemplateParseError, TemplateParseResult}; use crate::templater::{ - self, IntoTemplate, PlainTextFormattedProperty, Template, TemplateFormatter, TemplateProperty, + self, PlainTextFormattedProperty, Template, TemplateFormatter, TemplateProperty, TemplatePropertyError, TemplatePropertyExt as _, }; use crate::{revset_util, text_util}; diff --git a/cli/src/operation_templater.rs b/cli/src/operation_templater.rs index 4c884fd4c0..0a66dabce4 100644 --- a/cli/src/operation_templater.rs +++ b/cli/src/operation_templater.rs @@ -28,7 +28,7 @@ use crate::template_builder::{ }; use crate::template_parser::{self, FunctionCallNode, TemplateParseResult}; use crate::templater::{ - IntoTemplate, PlainTextFormattedProperty, Template, TemplateFormatter, TemplateProperty, + PlainTextFormattedProperty, Template, TemplateFormatter, TemplateProperty, TemplatePropertyExt as _, TimestampRange, }; diff --git a/cli/src/template_builder.rs b/cli/src/template_builder.rs index b70b9d6207..12fb42712d 100644 --- a/cli/src/template_builder.rs +++ b/cli/src/template_builder.rs @@ -22,10 +22,10 @@ use crate::template_parser::{ TemplateParseError, TemplateParseErrorKind, TemplateParseResult, UnaryOp, }; use crate::templater::{ - CoalesceTemplate, ConcatTemplate, ConditionalTemplate, IntoTemplate, LabelTemplate, - ListPropertyTemplate, ListTemplate, Literal, PlainTextFormattedProperty, PropertyPlaceholder, - ReformatTemplate, SeparateTemplate, Template, TemplateProperty, TemplatePropertyError, - TemplatePropertyExt as _, TemplateRenderer, TimestampRange, + CoalesceTemplate, ConcatTemplate, ConditionalTemplate, LabelTemplate, ListPropertyTemplate, + ListTemplate, Literal, PlainTextFormattedProperty, PropertyPlaceholder, ReformatTemplate, + SeparateTemplate, Template, TemplateProperty, TemplatePropertyError, TemplatePropertyExt as _, + TemplateRenderer, TimestampRange, }; use crate::{text_util, time_util}; diff --git a/cli/src/templater.rs b/cli/src/templater.rs index 284fcee64f..5c4a428189 100644 --- a/cli/src/templater.rs +++ b/cli/src/templater.rs @@ -39,10 +39,6 @@ pub trait ListTemplate: Template { Self: 'a; } -pub trait IntoTemplate<'a> { - fn into_template(self) -> Box; -} - impl Template for &T { fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> { ::format(self, formatter) @@ -352,6 +348,15 @@ pub trait TemplatePropertyExt: TemplateProperty { { TemplateFunction::new(self, move |value| Ok(function(value))) } + + /// Converts this property into `Template`. + fn into_template<'a>(self) -> Box + where + Self: Sized + 'a, + Self::Output: Template, + { + Box::new(FormattablePropertyTemplate::new(self)) + } } impl TemplatePropertyExt for P {} @@ -401,15 +406,6 @@ where } } -impl<'a, O> IntoTemplate<'a> for Box + 'a> -where - O: Template + 'a, -{ - fn into_template(self) -> Box { - Box::new(FormattablePropertyTemplate::new(self)) - } -} - /// Adapter to turn template back to string property. pub struct PlainTextFormattedProperty { template: T, From 8ae79f02c7e87074e341432afbace6bd5fc87250 Mon Sep 17 00:00:00 2001 From: Yuya Nishihara Date: Thu, 2 May 2024 13:31:20 +0900 Subject: [PATCH 2/4] templater: add helper method that unwraps Option property I'll add a few more optional property types, and I don't want to duplicate the error message. Type names are capitalized for consistency. --- cli/src/commit_templater.rs | 10 +++------- cli/src/templater.rs | 11 +++++++++++ cli/tests/test_global_opts.rs | 2 +- cli/tests/test_tag_command.rs | 2 +- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/cli/src/commit_templater.rs b/cli/src/commit_templater.rs index 46d203dc06..43a6d9d16a 100644 --- a/cli/src/commit_templater.rs +++ b/cli/src/commit_templater.rs @@ -38,7 +38,7 @@ use crate::template_builder::{ use crate::template_parser::{self, FunctionCallNode, TemplateParseError, TemplateParseResult}; use crate::templater::{ self, PlainTextFormattedProperty, Template, TemplateFormatter, TemplateProperty, - TemplatePropertyError, TemplatePropertyExt as _, + TemplatePropertyExt as _, }; use crate::{revset_util, text_util}; @@ -130,9 +130,7 @@ impl<'repo> TemplateLanguage<'repo> for CommitTemplateLanguage<'repo> { let type_name = "Commit"; let table = &self.build_fn_table.commit_methods; let build = template_parser::lookup_method(type_name, table, function)?; - let inner_property = property.and_then(|opt| { - opt.ok_or_else(|| TemplatePropertyError("No commit available".into())) - }); + let inner_property = property.try_unwrap(type_name); build(self, build_ctx, Box::new(inner_property), function) } CommitTemplatePropertyKind::CommitList(property) => { @@ -154,9 +152,7 @@ impl<'repo> TemplateLanguage<'repo> for CommitTemplateLanguage<'repo> { let type_name = "RefName"; let table = &self.build_fn_table.ref_name_methods; let build = template_parser::lookup_method(type_name, table, function)?; - let inner_property = property.and_then(|opt| { - opt.ok_or_else(|| TemplatePropertyError("No RefName available".into())) - }); + let inner_property = property.try_unwrap(type_name); build(self, build_ctx, Box::new(inner_property), function) } CommitTemplatePropertyKind::RefNameList(property) => { diff --git a/cli/src/templater.rs b/cli/src/templater.rs index 5c4a428189..87734c3ceb 100644 --- a/cli/src/templater.rs +++ b/cli/src/templater.rs @@ -349,6 +349,17 @@ pub trait TemplatePropertyExt: TemplateProperty { TemplateFunction::new(self, move |value| Ok(function(value))) } + /// Translates to a property that will unwrap an extracted `Option` value + /// of the specified `type_name`, mapping `None` to `Err`. + fn try_unwrap(self, type_name: &str) -> impl TemplateProperty + where + Self: TemplateProperty> + Sized, + { + self.and_then(move |opt| { + opt.ok_or_else(|| TemplatePropertyError(format!("No {type_name} available").into())) + }) + } + /// Converts this property into `Template`. fn into_template<'a>(self) -> Box where diff --git a/cli/tests/test_global_opts.rs b/cli/tests/test_global_opts.rs index a0c145de54..fe4567896f 100644 --- a/cli/tests/test_global_opts.rs +++ b/cli/tests/test_global_opts.rs @@ -473,7 +473,7 @@ fn test_color_ui_messages() { ); insta::assert_snapshot!(stdout, @r###" 167f90e7600a50f85c4f909b53eaf546faa82879 - <Error: No commit available> (elided revisions) + <Error: No Commit available> (elided revisions) 0000000000000000000000000000000000000000 "###); diff --git a/cli/tests/test_tag_command.rs b/cli/tests/test_tag_command.rs index 491c41c6ec..b74980ab73 100644 --- a/cli/tests/test_tag_command.rs +++ b/cli/tests/test_tag_command.rs @@ -100,7 +100,7 @@ fn test_tag_list() { [conflicted_tag] present: true conflict: true - normal_target: + normal_target: removed_targets: commit1 added_targets: commit2 commit3 [test_tag] From 42dfb5a7b66029150aaf68230ece5add9e119a7a Mon Sep 17 00:00:00 2001 From: Yuya Nishihara Date: Tue, 30 Apr 2024 13:55:38 +0900 Subject: [PATCH 3/4] templater: add optional integer type In order to port "branch list" to template, we need to somehow represent revset.count_estimate() result as a template property. I'm going to add SizeHint template type for that, and its .upper() and .exact() methods will have to return optional integers. Fortunately, the Integer type has no implicit conversion to bool, so "if(optional_integer, ..)" is not ambiguous. --- cli/src/template_builder.rs | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/cli/src/template_builder.rs b/cli/src/template_builder.rs index 12fb42712d..deae24d3e9 100644 --- a/cli/src/template_builder.rs +++ b/cli/src/template_builder.rs @@ -39,6 +39,9 @@ pub trait TemplateLanguage<'a> { ) -> Self::Property; fn wrap_boolean(property: impl TemplateProperty + 'a) -> Self::Property; fn wrap_integer(property: impl TemplateProperty + 'a) -> Self::Property; + fn wrap_integer_opt( + property: impl TemplateProperty> + 'a, + ) -> Self::Property; fn wrap_signature(property: impl TemplateProperty + 'a) -> Self::Property; fn wrap_timestamp(property: impl TemplateProperty + 'a) -> Self::Property; fn wrap_timestamp_range( @@ -81,6 +84,7 @@ macro_rules! impl_core_wrap_property_fns { wrap_string_list(Vec) => StringList, wrap_boolean(bool) => Boolean, wrap_integer(i64) => Integer, + wrap_integer_opt(Option) => IntegerOpt, wrap_signature(jj_lib::backend::Signature) => Signature, wrap_timestamp(jj_lib::backend::Timestamp) => Timestamp, wrap_timestamp_range($crate::templater::TimestampRange) => TimestampRange, @@ -133,6 +137,7 @@ pub enum CoreTemplatePropertyKind<'a> { StringList(Box> + 'a>), Boolean(Box + 'a>), Integer(Box + 'a>), + IntegerOpt(Box> + 'a>), Signature(Box + 'a>), Timestamp(Box + 'a>), TimestampRange(Box + 'a>), @@ -158,6 +163,7 @@ impl<'a> IntoTemplateProperty<'a> for CoreTemplatePropertyKind<'a> { CoreTemplatePropertyKind::StringList(_) => "List", CoreTemplatePropertyKind::Boolean(_) => "Boolean", CoreTemplatePropertyKind::Integer(_) => "Integer", + CoreTemplatePropertyKind::IntegerOpt(_) => "Option", CoreTemplatePropertyKind::Signature(_) => "Signature", CoreTemplatePropertyKind::Timestamp(_) => "Timestamp", CoreTemplatePropertyKind::TimestampRange(_) => "TimestampRange", @@ -176,6 +182,9 @@ impl<'a> IntoTemplateProperty<'a> for CoreTemplatePropertyKind<'a> { } CoreTemplatePropertyKind::Boolean(property) => Some(property), CoreTemplatePropertyKind::Integer(_) => None, + CoreTemplatePropertyKind::IntegerOpt(property) => { + Some(Box::new(property.map(|opt| opt.is_some()))) + } CoreTemplatePropertyKind::Signature(_) => None, CoreTemplatePropertyKind::Timestamp(_) => None, CoreTemplatePropertyKind::TimestampRange(_) => None, @@ -190,6 +199,9 @@ impl<'a> IntoTemplateProperty<'a> for CoreTemplatePropertyKind<'a> { fn try_into_integer(self) -> Option + 'a>> { match self { CoreTemplatePropertyKind::Integer(property) => Some(property), + CoreTemplatePropertyKind::IntegerOpt(property) => { + Some(Box::new(property.try_unwrap("Integer"))) + } _ => None, } } @@ -210,6 +222,7 @@ impl<'a> IntoTemplateProperty<'a> for CoreTemplatePropertyKind<'a> { CoreTemplatePropertyKind::StringList(property) => Some(property.into_template()), CoreTemplatePropertyKind::Boolean(property) => Some(property.into_template()), CoreTemplatePropertyKind::Integer(property) => Some(property.into_template()), + CoreTemplatePropertyKind::IntegerOpt(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()), @@ -357,6 +370,13 @@ impl<'a, L: TemplateLanguage<'a> + ?Sized> CoreTemplateBuildFnTable<'a, L> { let build = template_parser::lookup_method(type_name, table, function)?; build(language, 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); + build(language, build_ctx, Box::new(inner_property), function) + } CoreTemplatePropertyKind::Signature(property) => { let table = &self.signature_methods; let build = template_parser::lookup_method(type_name, table, function)?; @@ -1491,6 +1511,12 @@ mod tests { = 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", || L::wrap_integer_opt(Literal(None))); + env.add_keyword("some_i64", || L::wrap_integer_opt(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"); + insta::assert_snapshot!(env.parse_err(r#"if(label("", ""), true, false)"#), @r###" --> 1:4 | @@ -1512,12 +1538,19 @@ mod tests { #[test] fn test_arithmetic_operation() { let mut env = TestTemplateEnv::new(); + env.add_keyword("none_i64", || L::wrap_integer_opt(Literal(None))); + env.add_keyword("some_i64", || L::wrap_integer_opt(Literal(Some(1)))); env.add_keyword("i64_min", || L::wrap_integer(Literal(i64::MIN))); 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"); + // 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"#), @""); + insta::assert_snapshot!(env.render_ok(r#"-some_i64"#), @"-1"); + // No panic on integer overflow. insta::assert_snapshot!( env.render_ok(r#"-i64_min"#), From 6f654252fe2d411ae5b64b33db1482fc22de5af8 Mon Sep 17 00:00:00 2001 From: Yuya Nishihara Date: Tue, 30 Apr 2024 14:24:08 +0900 Subject: [PATCH 4/4] templater: add SizeHint type to represent revset.count_estimate() value We'll probably add binary comparison operators at some point, but this patch also adds size_hint.zero() method. Otherwise, we'll have to write "if(x.upper() && x.upper() == 0, ..)" to deal with None. The resulting "branch list" template will look like: ``` separate(", ", if(!ref.tracking_ahead_count().zero(), if(ref.tracking_ahead_count().exact(), "ahead by " ++ ref.tracking_ahead_count().exact() ++ " commits", "ahead by at least " ++ ref.tracking_ahead_count().lower() ++ " commits")), if(!ref.tracking_behind_count().zero(), if(ref.tracking_behind_count().exact(), "behind by " ++ ref.tracking_behind_count().exact() ++ " commits", "behind by at least " ++ ref.tracking_behind_count().lower() ++ " commits")), ) ``` --- cli/src/template_builder.rs | 75 ++++++++++++++++++++++++++++++++++++- cli/src/templater.rs | 5 +++ docs/templates.md | 10 +++++ 3 files changed, 88 insertions(+), 2 deletions(-) diff --git a/cli/src/template_builder.rs b/cli/src/template_builder.rs index deae24d3e9..31ae846756 100644 --- a/cli/src/template_builder.rs +++ b/cli/src/template_builder.rs @@ -24,8 +24,8 @@ use crate::template_parser::{ use crate::templater::{ CoalesceTemplate, ConcatTemplate, ConditionalTemplate, LabelTemplate, ListPropertyTemplate, ListTemplate, Literal, PlainTextFormattedProperty, PropertyPlaceholder, ReformatTemplate, - SeparateTemplate, Template, TemplateProperty, TemplatePropertyError, TemplatePropertyExt as _, - TemplateRenderer, TimestampRange, + SeparateTemplate, SizeHint, Template, TemplateProperty, TemplatePropertyError, + TemplatePropertyExt as _, TemplateRenderer, TimestampRange, }; use crate::{text_util, time_util}; @@ -43,6 +43,7 @@ pub trait TemplateLanguage<'a> { property: impl TemplateProperty> + 'a, ) -> Self::Property; fn wrap_signature(property: impl TemplateProperty + 'a) -> Self::Property; + fn wrap_size_hint(property: impl TemplateProperty + 'a) -> Self::Property; fn wrap_timestamp(property: impl TemplateProperty + 'a) -> Self::Property; fn wrap_timestamp_range( property: impl TemplateProperty + 'a, @@ -86,6 +87,7 @@ macro_rules! impl_core_wrap_property_fns { wrap_integer(i64) => Integer, wrap_integer_opt(Option) => IntegerOpt, wrap_signature(jj_lib::backend::Signature) => Signature, + wrap_size_hint($crate::templater::SizeHint) => SizeHint, wrap_timestamp(jj_lib::backend::Timestamp) => Timestamp, wrap_timestamp_range($crate::templater::TimestampRange) => TimestampRange, } @@ -139,6 +141,7 @@ pub enum CoreTemplatePropertyKind<'a> { Integer(Box + 'a>), IntegerOpt(Box> + 'a>), Signature(Box + 'a>), + SizeHint(Box + 'a>), Timestamp(Box + 'a>), TimestampRange(Box + 'a>), @@ -165,6 +168,7 @@ impl<'a> IntoTemplateProperty<'a> for CoreTemplatePropertyKind<'a> { CoreTemplatePropertyKind::Integer(_) => "Integer", CoreTemplatePropertyKind::IntegerOpt(_) => "Option", CoreTemplatePropertyKind::Signature(_) => "Signature", + CoreTemplatePropertyKind::SizeHint(_) => "SizeHint", CoreTemplatePropertyKind::Timestamp(_) => "Timestamp", CoreTemplatePropertyKind::TimestampRange(_) => "TimestampRange", CoreTemplatePropertyKind::Template(_) => "Template", @@ -186,6 +190,7 @@ impl<'a> IntoTemplateProperty<'a> for CoreTemplatePropertyKind<'a> { Some(Box::new(property.map(|opt| opt.is_some()))) } CoreTemplatePropertyKind::Signature(_) => None, + CoreTemplatePropertyKind::SizeHint(_) => None, CoreTemplatePropertyKind::Timestamp(_) => None, CoreTemplatePropertyKind::TimestampRange(_) => None, // Template types could also be evaluated to boolean, but it's less likely @@ -224,6 +229,7 @@ impl<'a> IntoTemplateProperty<'a> for CoreTemplatePropertyKind<'a> { CoreTemplatePropertyKind::Integer(property) => Some(property.into_template()), CoreTemplatePropertyKind::IntegerOpt(property) => Some(property.into_template()), CoreTemplatePropertyKind::Signature(property) => Some(property.into_template()), + CoreTemplatePropertyKind::SizeHint(_) => None, CoreTemplatePropertyKind::Timestamp(property) => Some(property.into_template()), CoreTemplatePropertyKind::TimestampRange(property) => Some(property.into_template()), CoreTemplatePropertyKind::Template(template) => Some(template), @@ -268,6 +274,7 @@ pub struct CoreTemplateBuildFnTable<'a, L: TemplateLanguage<'a> + ?Sized> { pub boolean_methods: TemplateBuildMethodFnMap<'a, L, bool>, pub integer_methods: TemplateBuildMethodFnMap<'a, L, i64>, pub signature_methods: TemplateBuildMethodFnMap<'a, L, Signature>, + pub size_hint_methods: TemplateBuildMethodFnMap<'a, L, SizeHint>, pub timestamp_methods: TemplateBuildMethodFnMap<'a, L, Timestamp>, pub timestamp_range_methods: TemplateBuildMethodFnMap<'a, L, TimestampRange>, } @@ -289,6 +296,7 @@ impl<'a, L: TemplateLanguage<'a> + ?Sized> CoreTemplateBuildFnTable<'a, L> { boolean_methods: HashMap::new(), integer_methods: HashMap::new(), signature_methods: builtin_signature_methods(), + size_hint_methods: builtin_size_hint_methods(), timestamp_methods: builtin_timestamp_methods(), timestamp_range_methods: builtin_timestamp_range_methods(), } @@ -301,6 +309,7 @@ impl<'a, L: TemplateLanguage<'a> + ?Sized> CoreTemplateBuildFnTable<'a, L> { boolean_methods: HashMap::new(), integer_methods: HashMap::new(), signature_methods: HashMap::new(), + size_hint_methods: HashMap::new(), timestamp_methods: HashMap::new(), timestamp_range_methods: HashMap::new(), } @@ -313,6 +322,7 @@ impl<'a, L: TemplateLanguage<'a> + ?Sized> CoreTemplateBuildFnTable<'a, L> { boolean_methods, integer_methods, signature_methods, + size_hint_methods, timestamp_methods, timestamp_range_methods, } = extension; @@ -322,6 +332,7 @@ impl<'a, L: TemplateLanguage<'a> + ?Sized> CoreTemplateBuildFnTable<'a, L> { 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.size_hint_methods, size_hint_methods); merge_fn_map(&mut self.timestamp_methods, timestamp_methods); merge_fn_map(&mut self.timestamp_range_methods, timestamp_range_methods); } @@ -382,6 +393,11 @@ impl<'a, L: TemplateLanguage<'a> + ?Sized> CoreTemplateBuildFnTable<'a, L> { let build = template_parser::lookup_method(type_name, table, function)?; build(language, 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, build_ctx, property, function) + } CoreTemplatePropertyKind::Timestamp(property) => { let table = &self.timestamp_methods; let build = template_parser::lookup_method(type_name, table, function)?; @@ -704,6 +720,38 @@ fn builtin_signature_methods<'a, L: TemplateLanguage<'a> + ?Sized>( 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::::new(); + map.insert("lower", |_language, _build_ctx, self_property, function| { + template_parser::expect_no_arguments(function)?; + let out_property = self_property.and_then(|(lower, _)| Ok(i64::try_from(lower)?)); + Ok(L::wrap_integer(out_property)) + }); + map.insert("upper", |_language, _build_ctx, self_property, function| { + template_parser::expect_no_arguments(function)?; + let out_property = + self_property.and_then(|(_, upper)| Ok(upper.map(i64::try_from).transpose()?)); + Ok(L::wrap_integer_opt(out_property)) + }); + map.insert("exact", |_language, _build_ctx, self_property, function| { + template_parser::expect_no_arguments(function)?; + 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(L::wrap_integer_opt(out_property)) + }); + map.insert("zero", |_language, _build_ctx, self_property, function| { + template_parser::expect_no_arguments(function)?; + let out_property = self_property.map(|(_, upper)| upper == Some(0)); + Ok(L::wrap_boolean(out_property)) + }); + map +} + fn builtin_timestamp_methods<'a, L: TemplateLanguage<'a> + ?Sized>( ) -> TemplateBuildMethodFnMap<'a, L, Timestamp> { // Not using maplit::hashmap!{} or custom declarative macro here because @@ -1847,6 +1895,29 @@ mod tests { insta::assert_snapshot!(env.render_ok(r#"author.username()"#), @""); } + #[test] + fn test_size_hint_method() { + let mut env = TestTemplateEnv::new(); + + env.add_keyword("unbounded", || L::wrap_size_hint(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", || L::wrap_size_hint(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", || L::wrap_size_hint(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(); diff --git a/cli/src/templater.rs b/cli/src/templater.rs index 87734c3ceb..b0b0f2cad2 100644 --- a/cli/src/templater.rs +++ b/cli/src/templater.rs @@ -74,6 +74,11 @@ impl Template for Signature { } } +// In template language, an integer value is represented as i64. However, we use +// usize here because it's more convenient to guarantee that the lower value is +// bounded to 0. +pub type SizeHint = (usize, Option); + impl Template for String { fn format(&self, formatter: &mut TemplateFormatter) -> io::Result<()> { write!(formatter, "{self}") diff --git a/docs/templates.md b/docs/templates.md index fe446db88e..2869f553ae 100644 --- a/docs/templates.md +++ b/docs/templates.md @@ -182,6 +182,16 @@ The following methods are defined. * `.username() -> String` * `.timestamp() -> Timestamp` +### SizeHint type + +This type cannot be printed. The following methods are defined. + +* `.lower() -> Integer`: Lower bound. +* `.upper() -> Option`: Upper bound if known. +* `.exact() -> Option`: Exact value if upper bound is known and it + equals to the lower bound. +* `.zero() -> Boolean`: True if upper bound is known and is `0`. + ### String type A string can be implicitly converted to `Boolean`. The following methods are