2022-11-26 23:57:50 +00:00
|
|
|
// Copyright 2020 The Jujutsu Authors
|
2020-12-12 08:00:42 +00:00
|
|
|
//
|
|
|
|
// 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.
|
|
|
|
|
2023-02-12 08:21:06 +00:00
|
|
|
use std::collections::HashMap;
|
2023-02-04 12:36:11 +00:00
|
|
|
use std::num::ParseIntError;
|
2023-02-03 05:36:01 +00:00
|
|
|
use std::ops::{RangeFrom, RangeInclusive};
|
2023-02-11 12:19:15 +00:00
|
|
|
use std::{error, fmt};
|
2023-02-02 08:57:55 +00:00
|
|
|
|
2023-01-28 11:09:13 +00:00
|
|
|
use itertools::Itertools as _;
|
2023-01-02 23:34:54 +00:00
|
|
|
use jujutsu_lib::backend::{Signature, Timestamp};
|
2021-05-15 16:16:31 +00:00
|
|
|
use jujutsu_lib::commit::Commit;
|
2022-02-02 18:14:03 +00:00
|
|
|
use jujutsu_lib::op_store::WorkspaceId;
|
2021-05-15 16:16:31 +00:00
|
|
|
use jujutsu_lib::repo::RepoRef;
|
2023-01-31 03:48:38 +00:00
|
|
|
use jujutsu_lib::rewrite;
|
2021-03-14 17:46:35 +00:00
|
|
|
use pest::iterators::{Pair, Pairs};
|
2020-12-12 08:00:42 +00:00
|
|
|
use pest::Parser;
|
2022-09-22 04:52:04 +00:00
|
|
|
use pest_derive::Parser;
|
2023-02-02 08:57:55 +00:00
|
|
|
use thiserror::Error;
|
2020-12-12 08:00:42 +00:00
|
|
|
|
|
|
|
use crate::templater::{
|
2023-01-26 11:00:52 +00:00
|
|
|
BranchProperty, CommitOrChangeId, ConditionalTemplate, FormattablePropertyTemplate,
|
2023-02-06 06:37:32 +00:00
|
|
|
GitHeadProperty, GitRefsProperty, LabelTemplate, ListTemplate, Literal,
|
|
|
|
PlainTextFormattedProperty, SeparateTemplate, ShortestIdPrefix, TagProperty, Template,
|
|
|
|
TemplateFunction, TemplateProperty, TemplatePropertyFn, WorkingCopiesProperty,
|
2020-12-12 08:00:42 +00:00
|
|
|
};
|
2023-01-31 03:48:38 +00:00
|
|
|
use crate::{cli_util, time_util};
|
2020-12-12 08:00:42 +00:00
|
|
|
|
|
|
|
#[derive(Parser)]
|
|
|
|
#[grammar = "template.pest"]
|
2023-02-12 14:15:44 +00:00
|
|
|
struct TemplateParser;
|
2020-12-12 08:00:42 +00:00
|
|
|
|
2023-02-03 10:42:03 +00:00
|
|
|
type TemplateParseResult<T> = Result<T, TemplateParseError>;
|
|
|
|
|
2023-02-02 08:57:55 +00:00
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
pub struct TemplateParseError {
|
|
|
|
kind: TemplateParseErrorKind,
|
|
|
|
pest_error: Box<pest::error::Error<Rule>>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, Eq, Error, PartialEq)]
|
|
|
|
pub enum TemplateParseErrorKind {
|
|
|
|
#[error("Syntax error")]
|
|
|
|
SyntaxError,
|
2023-02-04 12:36:11 +00:00
|
|
|
#[error("Invalid integer literal: {0}")]
|
|
|
|
ParseIntError(#[source] ParseIntError),
|
2023-02-02 10:31:50 +00:00
|
|
|
#[error(r#"Keyword "{0}" doesn't exist"#)]
|
|
|
|
NoSuchKeyword(String),
|
|
|
|
#[error(r#"Function "{0}" doesn't exist"#)]
|
|
|
|
NoSuchFunction(String),
|
|
|
|
#[error(r#"Method "{name}" doesn't exist for type "{type_name}""#)]
|
|
|
|
NoSuchMethod { type_name: String, name: String },
|
2023-02-03 15:01:53 +00:00
|
|
|
// TODO: clean up argument error variants
|
|
|
|
#[error("Expected {0} arguments")]
|
|
|
|
InvalidArgumentCountExact(usize),
|
|
|
|
#[error("Expected {} to {} arguments", .0.start(), .0.end())]
|
|
|
|
InvalidArgumentCountRange(RangeInclusive<usize>),
|
2023-02-03 05:36:01 +00:00
|
|
|
#[error("Expected at least {} arguments", .0.start)]
|
|
|
|
InvalidArgumentCountRangeFrom(RangeFrom<usize>),
|
2023-02-02 11:13:12 +00:00
|
|
|
#[error(r#"Expected argument of type "{0}""#)]
|
|
|
|
InvalidArgumentType(String),
|
2023-02-12 08:21:06 +00:00
|
|
|
#[error("Redefinition of function parameter")]
|
|
|
|
RedefinedFunctionParameter,
|
2023-02-02 10:31:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl TemplateParseError {
|
|
|
|
fn with_span(kind: TemplateParseErrorKind, span: pest::Span<'_>) -> Self {
|
|
|
|
let pest_error = Box::new(pest::error::Error::new_from_span(
|
|
|
|
pest::error::ErrorVariant::CustomError {
|
|
|
|
message: kind.to_string(),
|
|
|
|
},
|
|
|
|
span,
|
|
|
|
));
|
|
|
|
TemplateParseError { kind, pest_error }
|
|
|
|
}
|
|
|
|
|
2023-02-11 12:19:15 +00:00
|
|
|
fn no_such_keyword(name: impl Into<String>, span: pest::Span<'_>) -> Self {
|
|
|
|
TemplateParseError::with_span(TemplateParseErrorKind::NoSuchKeyword(name.into()), span)
|
2023-02-02 10:31:50 +00:00
|
|
|
}
|
|
|
|
|
2023-02-11 12:19:15 +00:00
|
|
|
fn no_such_function(function: &FunctionCallNode) -> Self {
|
2023-02-02 10:31:50 +00:00
|
|
|
TemplateParseError::with_span(
|
2023-02-11 12:19:15 +00:00
|
|
|
TemplateParseErrorKind::NoSuchFunction(function.name.to_owned()),
|
|
|
|
function.name_span,
|
2023-02-02 10:31:50 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-02-11 12:19:15 +00:00
|
|
|
fn no_such_method(type_name: impl Into<String>, function: &FunctionCallNode) -> Self {
|
2023-02-02 10:31:50 +00:00
|
|
|
TemplateParseError::with_span(
|
|
|
|
TemplateParseErrorKind::NoSuchMethod {
|
|
|
|
type_name: type_name.into(),
|
2023-02-11 12:19:15 +00:00
|
|
|
name: function.name.to_owned(),
|
2023-02-02 10:31:50 +00:00
|
|
|
},
|
2023-02-11 12:19:15 +00:00
|
|
|
function.name_span,
|
2023-02-02 10:31:50 +00:00
|
|
|
)
|
|
|
|
}
|
2023-02-02 11:13:12 +00:00
|
|
|
|
2023-02-03 15:01:53 +00:00
|
|
|
fn invalid_argument_count_exact(count: usize, span: pest::Span<'_>) -> Self {
|
2023-02-02 11:21:00 +00:00
|
|
|
TemplateParseError::with_span(
|
2023-02-03 15:01:53 +00:00
|
|
|
TemplateParseErrorKind::InvalidArgumentCountExact(count),
|
|
|
|
span,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn invalid_argument_count_range(count: RangeInclusive<usize>, span: pest::Span<'_>) -> Self {
|
|
|
|
TemplateParseError::with_span(
|
|
|
|
TemplateParseErrorKind::InvalidArgumentCountRange(count),
|
2023-02-02 11:21:00 +00:00
|
|
|
span,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-02-03 05:36:01 +00:00
|
|
|
fn invalid_argument_count_range_from(count: RangeFrom<usize>, span: pest::Span<'_>) -> Self {
|
|
|
|
TemplateParseError::with_span(
|
|
|
|
TemplateParseErrorKind::InvalidArgumentCountRangeFrom(count),
|
|
|
|
span,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-02-02 11:13:12 +00:00
|
|
|
fn invalid_argument_type(expected_type_name: impl Into<String>, span: pest::Span<'_>) -> Self {
|
|
|
|
TemplateParseError::with_span(
|
|
|
|
TemplateParseErrorKind::InvalidArgumentType(expected_type_name.into()),
|
|
|
|
span,
|
|
|
|
)
|
|
|
|
}
|
2023-02-02 08:57:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl From<pest::error::Error<Rule>> for TemplateParseError {
|
|
|
|
fn from(err: pest::error::Error<Rule>) -> Self {
|
|
|
|
TemplateParseError {
|
|
|
|
kind: TemplateParseErrorKind::SyntaxError,
|
|
|
|
pest_error: Box::new(err),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl fmt::Display for TemplateParseError {
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
self.pest_error.fmt(f)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl error::Error for TemplateParseError {
|
|
|
|
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
|
|
|
|
match &self.kind {
|
|
|
|
// SyntaxError is a wrapper for pest::error::Error.
|
|
|
|
TemplateParseErrorKind::SyntaxError => Some(&self.pest_error as &dyn error::Error),
|
|
|
|
// Otherwise the kind represents this error.
|
2023-02-02 10:31:50 +00:00
|
|
|
e => e.source(),
|
2023-02-02 08:57:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-11 11:24:26 +00:00
|
|
|
/// AST node without type or name checking.
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
2023-02-12 14:15:44 +00:00
|
|
|
pub struct ExpressionNode<'i> {
|
2023-02-11 11:24:26 +00:00
|
|
|
kind: ExpressionKind<'i>,
|
|
|
|
span: pest::Span<'i>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'i> ExpressionNode<'i> {
|
|
|
|
fn new(kind: ExpressionKind<'i>, span: pest::Span<'i>) -> Self {
|
|
|
|
ExpressionNode { kind, span }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
|
|
enum ExpressionKind<'i> {
|
|
|
|
Identifier(&'i str),
|
|
|
|
Integer(i64),
|
|
|
|
String(String),
|
|
|
|
List(Vec<ExpressionNode<'i>>),
|
|
|
|
FunctionCall(FunctionCallNode<'i>),
|
|
|
|
MethodCall(MethodCallNode<'i>),
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
|
|
struct FunctionCallNode<'i> {
|
|
|
|
name: &'i str,
|
|
|
|
name_span: pest::Span<'i>,
|
|
|
|
args: Vec<ExpressionNode<'i>>,
|
|
|
|
args_span: pest::Span<'i>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
|
|
struct MethodCallNode<'i> {
|
|
|
|
object: Box<ExpressionNode<'i>>,
|
|
|
|
function: FunctionCallNode<'i>,
|
|
|
|
}
|
|
|
|
|
2020-12-12 08:00:42 +00:00
|
|
|
fn parse_string_literal(pair: Pair<Rule>) -> String {
|
|
|
|
assert_eq!(pair.as_rule(), Rule::literal);
|
|
|
|
let mut result = String::new();
|
|
|
|
for part in pair.into_inner() {
|
|
|
|
match part.as_rule() {
|
|
|
|
Rule::raw_literal => {
|
|
|
|
result.push_str(part.as_str());
|
|
|
|
}
|
|
|
|
Rule::escape => match part.as_str().as_bytes()[1] as char {
|
|
|
|
'"' => result.push('"'),
|
|
|
|
'\\' => result.push('\\'),
|
|
|
|
'n' => result.push('\n'),
|
2022-12-15 02:30:06 +00:00
|
|
|
char => panic!("invalid escape: \\{char:?}"),
|
2020-12-12 08:00:42 +00:00
|
|
|
},
|
2022-12-15 02:30:06 +00:00
|
|
|
_ => panic!("unexpected part of string: {part:?}"),
|
2020-12-12 08:00:42 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
result
|
|
|
|
}
|
|
|
|
|
2023-02-11 11:24:26 +00:00
|
|
|
fn parse_function_call_node(pair: Pair<Rule>) -> TemplateParseResult<FunctionCallNode> {
|
|
|
|
assert_eq!(pair.as_rule(), Rule::function);
|
|
|
|
let mut inner = pair.into_inner();
|
|
|
|
let name = inner.next().unwrap();
|
|
|
|
let args_pair = inner.next().unwrap();
|
|
|
|
let args_span = args_pair.as_span();
|
|
|
|
assert_eq!(name.as_rule(), Rule::identifier);
|
|
|
|
assert_eq!(args_pair.as_rule(), Rule::function_arguments);
|
|
|
|
let args = args_pair
|
|
|
|
.into_inner()
|
|
|
|
.map(parse_template_node)
|
|
|
|
.try_collect()?;
|
|
|
|
Ok(FunctionCallNode {
|
|
|
|
name: name.as_str(),
|
|
|
|
name_span: name.as_span(),
|
|
|
|
args,
|
|
|
|
args_span,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
fn parse_term_node(pair: Pair<Rule>) -> TemplateParseResult<ExpressionNode> {
|
2023-02-11 14:44:24 +00:00
|
|
|
assert_eq!(pair.as_rule(), Rule::term);
|
|
|
|
let mut inner = pair.into_inner();
|
|
|
|
let expr = inner.next().unwrap();
|
2023-02-11 11:24:26 +00:00
|
|
|
let span = expr.as_span();
|
2023-02-11 14:44:24 +00:00
|
|
|
let primary = match expr.as_rule() {
|
|
|
|
Rule::literal => {
|
|
|
|
let text = parse_string_literal(expr);
|
2023-02-11 11:24:26 +00:00
|
|
|
ExpressionNode::new(ExpressionKind::String(text), span)
|
2023-02-11 14:44:24 +00:00
|
|
|
}
|
|
|
|
Rule::integer_literal => {
|
|
|
|
let value = expr.as_str().parse().map_err(|err| {
|
2023-02-11 11:24:26 +00:00
|
|
|
TemplateParseError::with_span(TemplateParseErrorKind::ParseIntError(err), span)
|
2023-02-11 14:44:24 +00:00
|
|
|
})?;
|
2023-02-11 11:24:26 +00:00
|
|
|
ExpressionNode::new(ExpressionKind::Integer(value), span)
|
2023-02-11 14:44:24 +00:00
|
|
|
}
|
2023-02-11 11:24:26 +00:00
|
|
|
Rule::identifier => ExpressionNode::new(ExpressionKind::Identifier(expr.as_str()), span),
|
2023-02-11 14:44:24 +00:00
|
|
|
Rule::function => {
|
2023-02-11 11:24:26 +00:00
|
|
|
let function = parse_function_call_node(expr)?;
|
|
|
|
ExpressionNode::new(ExpressionKind::FunctionCall(function), span)
|
2023-02-11 14:44:24 +00:00
|
|
|
}
|
2023-02-11 11:24:26 +00:00
|
|
|
Rule::template => parse_template_node(expr)?,
|
2023-02-11 14:44:24 +00:00
|
|
|
other => panic!("unexpected term: {other:?}"),
|
|
|
|
};
|
2023-02-11 11:24:26 +00:00
|
|
|
inner.try_fold(primary, |object, chain| {
|
|
|
|
assert_eq!(chain.as_rule(), Rule::function);
|
|
|
|
let span = chain.as_span();
|
|
|
|
let method = MethodCallNode {
|
|
|
|
object: Box::new(object),
|
|
|
|
function: parse_function_call_node(chain)?,
|
|
|
|
};
|
|
|
|
Ok(ExpressionNode::new(
|
|
|
|
ExpressionKind::MethodCall(method),
|
|
|
|
span,
|
|
|
|
))
|
|
|
|
})
|
2023-02-11 14:44:24 +00:00
|
|
|
}
|
|
|
|
|
2023-02-11 11:24:26 +00:00
|
|
|
fn parse_template_node(pair: Pair<Rule>) -> TemplateParseResult<ExpressionNode> {
|
2023-02-11 14:44:24 +00:00
|
|
|
assert_eq!(pair.as_rule(), Rule::template);
|
2023-02-11 11:24:26 +00:00
|
|
|
let span = pair.as_span();
|
2023-02-11 14:44:24 +00:00
|
|
|
let inner = pair.into_inner();
|
2023-02-11 11:24:26 +00:00
|
|
|
let mut nodes: Vec<_> = inner.map(parse_term_node).try_collect()?;
|
|
|
|
if nodes.len() == 1 {
|
|
|
|
Ok(nodes.pop().unwrap())
|
2023-02-11 14:44:24 +00:00
|
|
|
} else {
|
2023-02-11 11:24:26 +00:00
|
|
|
Ok(ExpressionNode::new(ExpressionKind::List(nodes), span))
|
2023-02-11 14:44:24 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-11 11:24:26 +00:00
|
|
|
/// Parses text into AST nodes. No type/name checking is made at this stage.
|
2023-02-12 14:15:44 +00:00
|
|
|
pub fn parse_template(template_text: &str) -> TemplateParseResult<ExpressionNode> {
|
2023-02-11 14:44:24 +00:00
|
|
|
let mut pairs: Pairs<Rule> = TemplateParser::parse(Rule::program, template_text)?;
|
|
|
|
let first_pair = pairs.next().unwrap();
|
|
|
|
if first_pair.as_rule() == Rule::EOI {
|
2023-02-11 11:24:26 +00:00
|
|
|
let span = first_pair.as_span();
|
|
|
|
Ok(ExpressionNode::new(ExpressionKind::List(Vec::new()), span))
|
2023-02-11 14:44:24 +00:00
|
|
|
} else {
|
2023-02-11 11:24:26 +00:00
|
|
|
parse_template_node(first_pair)
|
2023-02-11 14:44:24 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-12 08:21:06 +00:00
|
|
|
#[derive(Clone, Debug, Default)]
|
|
|
|
pub struct TemplateAliasesMap {
|
|
|
|
symbol_aliases: HashMap<String, String>,
|
|
|
|
function_aliases: HashMap<String, (Vec<String>, String)>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl TemplateAliasesMap {
|
|
|
|
pub fn new() -> Self {
|
|
|
|
Self::default()
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Adds new substitution rule `decl = defn`.
|
|
|
|
///
|
|
|
|
/// Returns error if `decl` is invalid. The `defn` part isn't checked. A bad
|
|
|
|
/// `defn` will be reported when the alias is substituted.
|
|
|
|
pub fn insert(
|
|
|
|
&mut self,
|
|
|
|
decl: impl AsRef<str>,
|
|
|
|
defn: impl Into<String>,
|
|
|
|
) -> TemplateParseResult<()> {
|
|
|
|
match TemplateAliasDeclaration::parse(decl.as_ref())? {
|
|
|
|
TemplateAliasDeclaration::Symbol(name) => {
|
|
|
|
self.symbol_aliases.insert(name, defn.into());
|
|
|
|
}
|
|
|
|
TemplateAliasDeclaration::Function(name, params) => {
|
|
|
|
self.function_aliases.insert(name, (params, defn.into()));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)] // TODO: remove
|
|
|
|
fn get_symbol(&self, name: &str) -> Option<(TemplateAliasId<'_>, &str)> {
|
|
|
|
self.symbol_aliases
|
|
|
|
.get_key_value(name)
|
|
|
|
.map(|(name, defn)| (TemplateAliasId::Symbol(name), defn.as_ref()))
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)] // TODO: remove
|
|
|
|
fn get_function(&self, name: &str) -> Option<(TemplateAliasId<'_>, &[String], &str)> {
|
|
|
|
self.function_aliases
|
|
|
|
.get_key_value(name)
|
|
|
|
.map(|(name, (params, defn))| {
|
|
|
|
(
|
|
|
|
TemplateAliasId::Function(name),
|
|
|
|
params.as_ref(),
|
|
|
|
defn.as_ref(),
|
|
|
|
)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Parsed declaration part of alias rule.
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
enum TemplateAliasDeclaration {
|
|
|
|
Symbol(String),
|
|
|
|
Function(String, Vec<String>),
|
|
|
|
}
|
|
|
|
|
|
|
|
impl TemplateAliasDeclaration {
|
|
|
|
fn parse(source: &str) -> TemplateParseResult<Self> {
|
|
|
|
let mut pairs = TemplateParser::parse(Rule::alias_declaration, source)?;
|
|
|
|
let first = pairs.next().unwrap();
|
|
|
|
match first.as_rule() {
|
|
|
|
Rule::identifier => Ok(TemplateAliasDeclaration::Symbol(first.as_str().to_owned())),
|
|
|
|
Rule::function_alias_declaration => {
|
|
|
|
let mut inner = first.into_inner();
|
|
|
|
let name_pair = inner.next().unwrap();
|
|
|
|
let params_pair = inner.next().unwrap();
|
|
|
|
let params_span = params_pair.as_span();
|
|
|
|
assert_eq!(name_pair.as_rule(), Rule::identifier);
|
|
|
|
assert_eq!(params_pair.as_rule(), Rule::formal_parameters);
|
|
|
|
let name = name_pair.as_str().to_owned();
|
|
|
|
let params = params_pair
|
|
|
|
.into_inner()
|
|
|
|
.map(|pair| match pair.as_rule() {
|
|
|
|
Rule::identifier => pair.as_str().to_owned(),
|
|
|
|
r => panic!("unexpected formal parameter rule {r:?}"),
|
|
|
|
})
|
|
|
|
.collect_vec();
|
|
|
|
if params.iter().all_unique() {
|
|
|
|
Ok(TemplateAliasDeclaration::Function(name, params))
|
|
|
|
} else {
|
|
|
|
Err(TemplateParseError::with_span(
|
|
|
|
TemplateParseErrorKind::RedefinedFunctionParameter,
|
|
|
|
params_span,
|
|
|
|
))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
r => panic!("unexpected alias declaration rule {r:?}"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Borrowed reference to identify alias expression.
|
|
|
|
#[cfg(test)] // TODO: remove
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
|
|
enum TemplateAliasId<'a> {
|
|
|
|
Symbol(&'a str),
|
|
|
|
Function(&'a str),
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)] // TODO: remove
|
|
|
|
impl fmt::Display for TemplateAliasId<'_> {
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
match self {
|
|
|
|
TemplateAliasId::Symbol(name) => write!(f, "{name}"),
|
|
|
|
TemplateAliasId::Function(name) => write!(f, "{name}()"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-07 02:04:08 +00:00
|
|
|
enum Property<'a, I> {
|
templater: turn output parameter of TemplateProperty into associated type
When implementing FormattablePropertyTemplate, I tried a generic 'property: P'
first, and I couldn't figure out how to constrain the output type.
impl<C, O, P> Template<C> for FormattablePropertyTemplate<P>
where
P: TemplateProperty<C, O>, // 'O' isn't constrained by type
O: Template<()>,
According to the book, the problem is that we can add multiple implementations
of 'TemplateProperty<C, *>'. Since TemplateProperty is basically a function
to extract data from 'C', I think the output parameter shouldn't be freely
chosen.
https://doc.rust-lang.org/book/ch19-03-advanced-traits.html
With this change, I can express the type constraint as follows:
impl<C, P> Template<C> for FormattablePropertyTemplate<P>
where
P: TemplateProperty<C>,
P::Output: Template<()>,
2023-01-23 06:26:27 +00:00
|
|
|
String(Box<dyn TemplateProperty<I, Output = String> + 'a>),
|
|
|
|
Boolean(Box<dyn TemplateProperty<I, Output = bool> + 'a>),
|
2023-02-04 12:36:11 +00:00
|
|
|
Integer(Box<dyn TemplateProperty<I, Output = i64> + 'a>),
|
2023-01-23 06:51:01 +00:00
|
|
|
CommitOrChangeId(Box<dyn TemplateProperty<I, Output = CommitOrChangeId<'a>> + 'a>),
|
2023-02-06 06:37:32 +00:00
|
|
|
ShortestIdPrefix(Box<dyn TemplateProperty<I, Output = ShortestIdPrefix> + 'a>),
|
templater: turn output parameter of TemplateProperty into associated type
When implementing FormattablePropertyTemplate, I tried a generic 'property: P'
first, and I couldn't figure out how to constrain the output type.
impl<C, O, P> Template<C> for FormattablePropertyTemplate<P>
where
P: TemplateProperty<C, O>, // 'O' isn't constrained by type
O: Template<()>,
According to the book, the problem is that we can add multiple implementations
of 'TemplateProperty<C, *>'. Since TemplateProperty is basically a function
to extract data from 'C', I think the output parameter shouldn't be freely
chosen.
https://doc.rust-lang.org/book/ch19-03-advanced-traits.html
With this change, I can express the type constraint as follows:
impl<C, P> Template<C> for FormattablePropertyTemplate<P>
where
P: TemplateProperty<C>,
P::Output: Template<()>,
2023-01-23 06:26:27 +00:00
|
|
|
Signature(Box<dyn TemplateProperty<I, Output = Signature> + 'a>),
|
|
|
|
Timestamp(Box<dyn TemplateProperty<I, Output = Timestamp> + 'a>),
|
2023-01-07 02:04:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a, I: 'a> Property<'a, I> {
|
2023-01-30 10:52:41 +00:00
|
|
|
fn try_into_boolean(self) -> Option<Box<dyn TemplateProperty<I, Output = bool> + 'a>> {
|
|
|
|
match self {
|
|
|
|
Property::String(property) => {
|
|
|
|
Some(Box::new(TemplateFunction::new(property, |s| !s.is_empty())))
|
|
|
|
}
|
|
|
|
Property::Boolean(property) => Some(property),
|
|
|
|
_ => None,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-04 12:36:11 +00:00
|
|
|
fn try_into_integer(self) -> Option<Box<dyn TemplateProperty<I, Output = i64> + 'a>> {
|
|
|
|
match self {
|
|
|
|
Property::Integer(property) => Some(property),
|
|
|
|
_ => None,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-26 11:20:43 +00:00
|
|
|
fn into_plain_text(self) -> Box<dyn TemplateProperty<I, Output = String> + 'a> {
|
|
|
|
match self {
|
|
|
|
Property::String(property) => property,
|
|
|
|
_ => Box::new(PlainTextFormattedProperty::new(self.into_template())),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-22 23:02:46 +00:00
|
|
|
fn into_template(self) -> Box<dyn Template<I> + 'a> {
|
|
|
|
fn wrap<'a, I: 'a, O: Template<()> + 'a>(
|
templater: turn output parameter of TemplateProperty into associated type
When implementing FormattablePropertyTemplate, I tried a generic 'property: P'
first, and I couldn't figure out how to constrain the output type.
impl<C, O, P> Template<C> for FormattablePropertyTemplate<P>
where
P: TemplateProperty<C, O>, // 'O' isn't constrained by type
O: Template<()>,
According to the book, the problem is that we can add multiple implementations
of 'TemplateProperty<C, *>'. Since TemplateProperty is basically a function
to extract data from 'C', I think the output parameter shouldn't be freely
chosen.
https://doc.rust-lang.org/book/ch19-03-advanced-traits.html
With this change, I can express the type constraint as follows:
impl<C, P> Template<C> for FormattablePropertyTemplate<P>
where
P: TemplateProperty<C>,
P::Output: Template<()>,
2023-01-23 06:26:27 +00:00
|
|
|
property: Box<dyn TemplateProperty<I, Output = O> + 'a>,
|
2023-01-22 23:02:46 +00:00
|
|
|
) -> Box<dyn Template<I> + 'a> {
|
|
|
|
Box::new(FormattablePropertyTemplate::new(property))
|
|
|
|
}
|
|
|
|
match self {
|
|
|
|
Property::String(property) => wrap(property),
|
|
|
|
Property::Boolean(property) => wrap(property),
|
2023-02-04 12:36:11 +00:00
|
|
|
Property::Integer(property) => wrap(property),
|
2023-01-23 06:51:01 +00:00
|
|
|
Property::CommitOrChangeId(property) => wrap(property),
|
2023-02-06 06:37:32 +00:00
|
|
|
Property::ShortestIdPrefix(property) => wrap(property),
|
2023-01-22 23:02:46 +00:00
|
|
|
Property::Signature(property) => wrap(property),
|
|
|
|
Property::Timestamp(property) => wrap(property),
|
|
|
|
}
|
|
|
|
}
|
2023-01-07 02:04:08 +00:00
|
|
|
}
|
|
|
|
|
2023-01-29 03:34:24 +00:00
|
|
|
struct PropertyAndLabels<'a, C>(Property<'a, C>, Vec<String>);
|
|
|
|
|
2023-01-31 02:42:26 +00:00
|
|
|
impl<'a, C: 'a> PropertyAndLabels<'a, C> {
|
|
|
|
fn into_template(self) -> Box<dyn Template<C> + 'a> {
|
|
|
|
let PropertyAndLabels(property, labels) = self;
|
|
|
|
if labels.is_empty() {
|
|
|
|
property.into_template()
|
|
|
|
} else {
|
2023-01-26 11:00:52 +00:00
|
|
|
Box::new(LabelTemplate::new(
|
|
|
|
property.into_template(),
|
|
|
|
Literal(labels),
|
|
|
|
))
|
2023-01-31 02:42:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-28 23:54:53 +00:00
|
|
|
enum Expression<'a, C> {
|
|
|
|
Property(PropertyAndLabels<'a, C>),
|
|
|
|
Template(Box<dyn Template<C> + 'a>),
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a, C: 'a> Expression<'a, C> {
|
2023-01-29 03:15:17 +00:00
|
|
|
fn try_into_boolean(self) -> Option<Box<dyn TemplateProperty<C, Output = bool> + 'a>> {
|
|
|
|
match self {
|
|
|
|
Expression::Property(PropertyAndLabels(property, _)) => property.try_into_boolean(),
|
|
|
|
Expression::Template(_) => None,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-04 12:36:11 +00:00
|
|
|
fn try_into_integer(self) -> Option<Box<dyn TemplateProperty<C, Output = i64> + 'a>> {
|
|
|
|
match self {
|
|
|
|
Expression::Property(PropertyAndLabels(property, _)) => property.try_into_integer(),
|
|
|
|
Expression::Template(_) => None,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-26 11:20:43 +00:00
|
|
|
fn into_plain_text(self) -> Box<dyn TemplateProperty<C, Output = String> + 'a> {
|
|
|
|
match self {
|
|
|
|
Expression::Property(PropertyAndLabels(property, _)) => property.into_plain_text(),
|
|
|
|
Expression::Template(template) => Box::new(PlainTextFormattedProperty::new(template)),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-28 23:54:53 +00:00
|
|
|
fn into_template(self) -> Box<dyn Template<C> + 'a> {
|
|
|
|
match self {
|
|
|
|
Expression::Property(property_labels) => property_labels.into_template(),
|
|
|
|
Expression::Template(template) => template,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-11 12:19:15 +00:00
|
|
|
fn expect_no_arguments(function: &FunctionCallNode) -> TemplateParseResult<()> {
|
|
|
|
if function.args.is_empty() {
|
2023-02-04 08:26:00 +00:00
|
|
|
Ok(())
|
|
|
|
} else {
|
2023-02-11 12:19:15 +00:00
|
|
|
Err(TemplateParseError::invalid_argument_count_exact(
|
|
|
|
0,
|
|
|
|
function.args_span,
|
|
|
|
))
|
2023-02-04 08:26:00 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-02 12:30:49 +00:00
|
|
|
/// Extracts exactly N required arguments.
|
2023-02-11 12:19:15 +00:00
|
|
|
fn expect_exact_arguments<'a, 'i, const N: usize>(
|
|
|
|
function: &'a FunctionCallNode<'i>,
|
|
|
|
) -> TemplateParseResult<&'a [ExpressionNode<'i>; N]> {
|
|
|
|
function
|
|
|
|
.args
|
|
|
|
.as_slice()
|
2023-02-02 12:30:49 +00:00
|
|
|
.try_into()
|
2023-02-11 12:19:15 +00:00
|
|
|
.map_err(|_| TemplateParseError::invalid_argument_count_exact(N, function.args_span))
|
2023-02-02 12:30:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Extracts N required arguments and remainders.
|
2023-02-11 12:19:15 +00:00
|
|
|
fn expect_some_arguments<'a, 'i, const N: usize>(
|
|
|
|
function: &'a FunctionCallNode<'i>,
|
|
|
|
) -> TemplateParseResult<(&'a [ExpressionNode<'i>; N], &'a [ExpressionNode<'i>])> {
|
|
|
|
if function.args.len() >= N {
|
|
|
|
let (required, rest) = function.args.split_at(N);
|
|
|
|
Ok((required.try_into().unwrap(), rest))
|
|
|
|
} else {
|
|
|
|
Err(TemplateParseError::invalid_argument_count_range_from(
|
|
|
|
N..,
|
|
|
|
function.args_span,
|
|
|
|
))
|
|
|
|
}
|
2023-02-02 12:30:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Extracts N required arguments and M optional arguments.
|
2023-02-11 12:19:15 +00:00
|
|
|
fn expect_arguments<'a, 'i, const N: usize, const M: usize>(
|
|
|
|
function: &'a FunctionCallNode<'i>,
|
|
|
|
) -> TemplateParseResult<(
|
|
|
|
&'a [ExpressionNode<'i>; N],
|
|
|
|
[Option<&'a ExpressionNode<'i>>; M],
|
|
|
|
)> {
|
|
|
|
let count_range = N..=(N + M);
|
|
|
|
if count_range.contains(&function.args.len()) {
|
|
|
|
let (required, rest) = function.args.split_at(N);
|
|
|
|
let mut optional = rest.iter().map(Some).collect_vec();
|
|
|
|
optional.resize(M, None);
|
|
|
|
Ok((required.try_into().unwrap(), optional.try_into().unwrap()))
|
2023-02-02 12:30:49 +00:00
|
|
|
} else {
|
2023-02-11 12:19:15 +00:00
|
|
|
Err(TemplateParseError::invalid_argument_count_range(
|
|
|
|
count_range,
|
|
|
|
function.args_span,
|
|
|
|
))
|
2023-02-02 12:30:49 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-10 17:01:28 +00:00
|
|
|
fn split_email(email: &str) -> (&str, Option<&str>) {
|
|
|
|
if let Some((username, rest)) = email.split_once('@') {
|
|
|
|
(username, Some(rest))
|
|
|
|
} else {
|
|
|
|
(email, None)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-11 12:19:15 +00:00
|
|
|
fn build_method_call<'a, I: 'a>(
|
|
|
|
method: &MethodCallNode,
|
|
|
|
build_keyword: &impl Fn(&str, pest::Span) -> TemplateParseResult<PropertyAndLabels<'a, I>>,
|
|
|
|
) -> TemplateParseResult<Expression<'a, I>> {
|
|
|
|
match build_expression(&method.object, build_keyword)? {
|
|
|
|
Expression::Property(PropertyAndLabels(property, mut labels)) => {
|
|
|
|
let property = match property {
|
|
|
|
Property::String(property) => {
|
|
|
|
build_string_method(property, &method.function, build_keyword)?
|
|
|
|
}
|
|
|
|
Property::Boolean(property) => {
|
|
|
|
build_boolean_method(property, &method.function, build_keyword)?
|
|
|
|
}
|
|
|
|
Property::Integer(property) => {
|
|
|
|
build_integer_method(property, &method.function, build_keyword)?
|
|
|
|
}
|
|
|
|
Property::CommitOrChangeId(property) => {
|
|
|
|
build_commit_or_change_id_method(property, &method.function, build_keyword)?
|
|
|
|
}
|
|
|
|
Property::ShortestIdPrefix(property) => {
|
|
|
|
build_shortest_id_prefix_method(property, &method.function, build_keyword)?
|
|
|
|
}
|
|
|
|
Property::Signature(property) => {
|
|
|
|
build_signature_method(property, &method.function, build_keyword)?
|
|
|
|
}
|
|
|
|
Property::Timestamp(property) => {
|
|
|
|
build_timestamp_method(property, &method.function, build_keyword)?
|
|
|
|
}
|
|
|
|
};
|
|
|
|
labels.push(method.function.name.to_owned());
|
|
|
|
Ok(Expression::Property(PropertyAndLabels(property, labels)))
|
|
|
|
}
|
|
|
|
Expression::Template(_) => Err(TemplateParseError::no_such_method(
|
|
|
|
"Template",
|
|
|
|
&method.function,
|
|
|
|
)),
|
2020-12-12 08:00:42 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-03 11:28:44 +00:00
|
|
|
fn chain_properties<'a, I: 'a, J: 'a, O: 'a>(
|
|
|
|
first: impl TemplateProperty<I, Output = J> + 'a,
|
|
|
|
second: impl TemplateProperty<J, Output = O> + 'a,
|
|
|
|
) -> Box<dyn TemplateProperty<I, Output = O> + 'a> {
|
|
|
|
Box::new(TemplateFunction::new(first, move |value| {
|
|
|
|
second.extract(&value)
|
|
|
|
}))
|
|
|
|
}
|
|
|
|
|
2023-02-11 12:19:15 +00:00
|
|
|
fn build_string_method<'a, I: 'a>(
|
2023-02-03 11:28:44 +00:00
|
|
|
self_property: impl TemplateProperty<I, Output = String> + 'a,
|
2023-02-11 12:19:15 +00:00
|
|
|
function: &FunctionCallNode,
|
|
|
|
build_keyword: &impl Fn(&str, pest::Span) -> TemplateParseResult<PropertyAndLabels<'a, I>>,
|
2023-02-03 11:28:44 +00:00
|
|
|
) -> TemplateParseResult<Property<'a, I>> {
|
2023-02-11 12:19:15 +00:00
|
|
|
let property = match function.name {
|
2023-02-04 07:38:39 +00:00
|
|
|
"contains" => {
|
2023-02-11 12:19:15 +00:00
|
|
|
let [needle_node] = expect_exact_arguments(function)?;
|
2023-02-04 07:38:39 +00:00
|
|
|
// TODO: or .try_into_string() to disable implicit type cast?
|
2023-02-11 12:19:15 +00:00
|
|
|
let needle_property = build_expression(needle_node, build_keyword)?.into_plain_text();
|
2023-02-04 07:38:39 +00:00
|
|
|
Property::Boolean(chain_properties(
|
|
|
|
(self_property, needle_property),
|
|
|
|
TemplatePropertyFn(|(haystack, needle): &(String, String)| {
|
|
|
|
haystack.contains(needle)
|
|
|
|
}),
|
|
|
|
))
|
|
|
|
}
|
2023-02-04 08:26:00 +00:00
|
|
|
"first_line" => {
|
2023-02-11 12:19:15 +00:00
|
|
|
expect_no_arguments(function)?;
|
2023-02-04 08:26:00 +00:00
|
|
|
Property::String(chain_properties(
|
|
|
|
self_property,
|
|
|
|
TemplatePropertyFn(|s: &String| s.lines().next().unwrap_or_default().to_string()),
|
|
|
|
))
|
|
|
|
}
|
2023-02-11 12:19:15 +00:00
|
|
|
_ => return Err(TemplateParseError::no_such_method("String", function)),
|
2023-02-02 10:36:33 +00:00
|
|
|
};
|
|
|
|
Ok(property)
|
2020-12-12 08:00:42 +00:00
|
|
|
}
|
|
|
|
|
2023-02-11 12:19:15 +00:00
|
|
|
fn build_boolean_method<'a, I: 'a>(
|
2023-02-03 11:28:44 +00:00
|
|
|
_self_property: impl TemplateProperty<I, Output = bool> + 'a,
|
2023-02-11 12:19:15 +00:00
|
|
|
function: &FunctionCallNode,
|
|
|
|
_build_keyword: &impl Fn(&str, pest::Span) -> TemplateParseResult<PropertyAndLabels<'a, I>>,
|
2023-02-03 11:28:44 +00:00
|
|
|
) -> TemplateParseResult<Property<'a, I>> {
|
2023-02-11 12:19:15 +00:00
|
|
|
Err(TemplateParseError::no_such_method("Boolean", function))
|
2020-12-12 08:00:42 +00:00
|
|
|
}
|
|
|
|
|
2023-02-11 12:19:15 +00:00
|
|
|
fn build_integer_method<'a, I: 'a>(
|
2023-02-04 12:36:11 +00:00
|
|
|
_self_property: impl TemplateProperty<I, Output = i64> + 'a,
|
2023-02-11 12:19:15 +00:00
|
|
|
function: &FunctionCallNode,
|
|
|
|
_build_keyword: &impl Fn(&str, pest::Span) -> TemplateParseResult<PropertyAndLabels<'a, I>>,
|
2023-02-04 12:36:11 +00:00
|
|
|
) -> TemplateParseResult<Property<'a, I>> {
|
2023-02-11 12:19:15 +00:00
|
|
|
Err(TemplateParseError::no_such_method("Integer", function))
|
2023-02-04 12:36:11 +00:00
|
|
|
}
|
|
|
|
|
2023-02-11 12:19:15 +00:00
|
|
|
fn build_commit_or_change_id_method<'a, I: 'a>(
|
2023-02-03 11:28:44 +00:00
|
|
|
self_property: impl TemplateProperty<I, Output = CommitOrChangeId<'a>> + 'a,
|
2023-02-11 12:19:15 +00:00
|
|
|
function: &FunctionCallNode,
|
|
|
|
build_keyword: &impl Fn(&str, pest::Span) -> TemplateParseResult<PropertyAndLabels<'a, I>>,
|
2023-02-03 11:28:44 +00:00
|
|
|
) -> TemplateParseResult<Property<'a, I>> {
|
2023-02-11 12:19:15 +00:00
|
|
|
let parse_optional_integer = |function| -> Result<Option<_>, TemplateParseError> {
|
|
|
|
let ([], [len_node]) = expect_arguments(function)?;
|
|
|
|
len_node
|
|
|
|
.map(|node| {
|
|
|
|
build_expression(node, build_keyword).and_then(|p| {
|
|
|
|
p.try_into_integer().ok_or_else(|| {
|
|
|
|
TemplateParseError::invalid_argument_type("Integer", node.span)
|
|
|
|
})
|
2023-01-21 04:56:34 +00:00
|
|
|
})
|
|
|
|
})
|
|
|
|
.transpose()
|
|
|
|
};
|
2023-02-11 12:19:15 +00:00
|
|
|
let property = match function.name {
|
2023-02-04 08:26:00 +00:00
|
|
|
"short" => {
|
2023-02-11 12:19:15 +00:00
|
|
|
let len_property = parse_optional_integer(function)?;
|
2023-02-04 08:26:00 +00:00
|
|
|
Property::String(chain_properties(
|
2023-02-03 06:48:39 +00:00
|
|
|
(self_property, len_property),
|
|
|
|
TemplatePropertyFn(|(id, len): &(CommitOrChangeId, Option<i64>)| {
|
|
|
|
id.short(len.and_then(|l| l.try_into().ok()).unwrap_or(12))
|
|
|
|
}),
|
2023-02-04 08:26:00 +00:00
|
|
|
))
|
|
|
|
}
|
2023-02-06 06:37:32 +00:00
|
|
|
"shortest" => {
|
2023-02-11 12:19:15 +00:00
|
|
|
let len_property = parse_optional_integer(function)?;
|
2023-02-06 06:37:32 +00:00
|
|
|
Property::ShortestIdPrefix(chain_properties(
|
2023-01-21 04:56:34 +00:00
|
|
|
(self_property, len_property),
|
|
|
|
TemplatePropertyFn(|(id, len): &(CommitOrChangeId, Option<i64>)| {
|
2023-02-06 06:52:16 +00:00
|
|
|
id.shortest(len.and_then(|l| l.try_into().ok()).unwrap_or(0))
|
2023-01-21 04:56:34 +00:00
|
|
|
}),
|
2023-02-04 08:26:00 +00:00
|
|
|
))
|
|
|
|
}
|
2023-02-02 10:31:50 +00:00
|
|
|
_ => {
|
|
|
|
return Err(TemplateParseError::no_such_method(
|
|
|
|
"CommitOrChangeId",
|
2023-02-11 12:19:15 +00:00
|
|
|
function,
|
|
|
|
))
|
2023-02-02 10:31:50 +00:00
|
|
|
}
|
2023-02-02 10:36:33 +00:00
|
|
|
};
|
|
|
|
Ok(property)
|
2020-12-12 08:00:42 +00:00
|
|
|
}
|
|
|
|
|
2023-02-11 12:19:15 +00:00
|
|
|
fn build_shortest_id_prefix_method<'a, I: 'a>(
|
2023-02-06 06:43:24 +00:00
|
|
|
self_property: impl TemplateProperty<I, Output = ShortestIdPrefix> + 'a,
|
2023-02-11 12:19:15 +00:00
|
|
|
function: &FunctionCallNode,
|
|
|
|
_build_keyword: &impl Fn(&str, pest::Span) -> TemplateParseResult<PropertyAndLabels<'a, I>>,
|
2023-02-06 06:43:24 +00:00
|
|
|
) -> TemplateParseResult<Property<'a, I>> {
|
2023-02-11 12:19:15 +00:00
|
|
|
let property = match function.name {
|
2023-02-06 06:43:24 +00:00
|
|
|
"with_brackets" => {
|
|
|
|
// TODO: If we had a map function, this could be expressed as a template
|
|
|
|
// like 'id.shortest() % (.prefix() if(.rest(), "[" .rest() "]"))'
|
2023-02-11 12:19:15 +00:00
|
|
|
expect_no_arguments(function)?;
|
2023-02-06 06:43:24 +00:00
|
|
|
Property::String(chain_properties(
|
|
|
|
self_property,
|
|
|
|
TemplatePropertyFn(|id: &ShortestIdPrefix| id.with_brackets()),
|
|
|
|
))
|
|
|
|
}
|
|
|
|
_ => {
|
|
|
|
return Err(TemplateParseError::no_such_method(
|
|
|
|
"ShortestIdPrefix",
|
2023-02-11 12:19:15 +00:00
|
|
|
function,
|
|
|
|
))
|
2023-02-06 06:43:24 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
Ok(property)
|
|
|
|
}
|
|
|
|
|
2023-02-11 12:19:15 +00:00
|
|
|
fn build_signature_method<'a, I: 'a>(
|
2023-02-03 11:28:44 +00:00
|
|
|
self_property: impl TemplateProperty<I, Output = Signature> + 'a,
|
2023-02-11 12:19:15 +00:00
|
|
|
function: &FunctionCallNode,
|
|
|
|
_build_keyword: &impl Fn(&str, pest::Span) -> TemplateParseResult<PropertyAndLabels<'a, I>>,
|
2023-02-03 11:28:44 +00:00
|
|
|
) -> TemplateParseResult<Property<'a, I>> {
|
2023-02-11 12:19:15 +00:00
|
|
|
let property = match function.name {
|
2023-02-04 08:26:00 +00:00
|
|
|
"name" => {
|
2023-02-11 12:19:15 +00:00
|
|
|
expect_no_arguments(function)?;
|
2023-02-04 08:26:00 +00:00
|
|
|
Property::String(chain_properties(
|
|
|
|
self_property,
|
|
|
|
TemplatePropertyFn(|signature: &Signature| signature.name.clone()),
|
|
|
|
))
|
|
|
|
}
|
|
|
|
"email" => {
|
2023-02-11 12:19:15 +00:00
|
|
|
expect_no_arguments(function)?;
|
2023-02-04 08:26:00 +00:00
|
|
|
Property::String(chain_properties(
|
|
|
|
self_property,
|
|
|
|
TemplatePropertyFn(|signature: &Signature| signature.email.clone()),
|
|
|
|
))
|
|
|
|
}
|
2023-02-10 17:01:28 +00:00
|
|
|
"username" => {
|
2023-02-11 12:19:15 +00:00
|
|
|
expect_no_arguments(function)?;
|
2023-02-10 17:01:28 +00:00
|
|
|
Property::String(chain_properties(
|
|
|
|
self_property,
|
|
|
|
TemplatePropertyFn(|signature: &Signature| {
|
|
|
|
let (username, _) = split_email(&signature.email);
|
|
|
|
username.to_owned()
|
|
|
|
}),
|
|
|
|
))
|
|
|
|
}
|
2023-02-04 08:26:00 +00:00
|
|
|
"timestamp" => {
|
2023-02-11 12:19:15 +00:00
|
|
|
expect_no_arguments(function)?;
|
2023-02-04 08:26:00 +00:00
|
|
|
Property::Timestamp(chain_properties(
|
|
|
|
self_property,
|
|
|
|
TemplatePropertyFn(|signature: &Signature| signature.timestamp.clone()),
|
|
|
|
))
|
|
|
|
}
|
2023-02-11 12:19:15 +00:00
|
|
|
_ => return Err(TemplateParseError::no_such_method("Signature", function)),
|
2023-02-02 10:36:33 +00:00
|
|
|
};
|
|
|
|
Ok(property)
|
2020-12-12 08:00:42 +00:00
|
|
|
}
|
|
|
|
|
2023-02-11 12:19:15 +00:00
|
|
|
fn build_timestamp_method<'a, I: 'a>(
|
2023-02-03 11:28:44 +00:00
|
|
|
self_property: impl TemplateProperty<I, Output = Timestamp> + 'a,
|
2023-02-11 12:19:15 +00:00
|
|
|
function: &FunctionCallNode,
|
|
|
|
_build_keyword: &impl Fn(&str, pest::Span) -> TemplateParseResult<PropertyAndLabels<'a, I>>,
|
2023-02-03 11:28:44 +00:00
|
|
|
) -> TemplateParseResult<Property<'a, I>> {
|
2023-02-11 12:19:15 +00:00
|
|
|
let property = match function.name {
|
2023-02-04 08:26:00 +00:00
|
|
|
"ago" => {
|
2023-02-11 12:19:15 +00:00
|
|
|
expect_no_arguments(function)?;
|
2023-02-04 08:26:00 +00:00
|
|
|
Property::String(chain_properties(
|
|
|
|
self_property,
|
|
|
|
TemplatePropertyFn(time_util::format_timestamp_relative_to_now),
|
|
|
|
))
|
|
|
|
}
|
2023-02-11 12:19:15 +00:00
|
|
|
_ => return Err(TemplateParseError::no_such_method("Timestamp", function)),
|
2023-02-02 10:36:33 +00:00
|
|
|
};
|
|
|
|
Ok(property)
|
2022-11-26 01:33:24 +00:00
|
|
|
}
|
|
|
|
|
2023-02-11 12:19:15 +00:00
|
|
|
fn build_global_function<'a, C: 'a>(
|
|
|
|
function: &FunctionCallNode,
|
|
|
|
build_keyword: &impl Fn(&str, pest::Span) -> TemplateParseResult<PropertyAndLabels<'a, C>>,
|
2023-02-05 03:53:03 +00:00
|
|
|
) -> TemplateParseResult<Expression<'a, C>> {
|
2023-02-11 12:19:15 +00:00
|
|
|
let expression = match function.name {
|
2023-02-05 03:53:03 +00:00
|
|
|
"label" => {
|
2023-02-11 12:19:15 +00:00
|
|
|
let [label_node, content_node] = expect_exact_arguments(function)?;
|
|
|
|
let label_property = build_expression(label_node, build_keyword)?.into_plain_text();
|
|
|
|
let content = build_expression(content_node, build_keyword)?.into_template();
|
2023-02-05 03:53:03 +00:00
|
|
|
let labels = TemplateFunction::new(label_property, |s| {
|
|
|
|
s.split_whitespace().map(ToString::to_string).collect()
|
|
|
|
});
|
|
|
|
let template = Box::new(LabelTemplate::new(content, labels));
|
|
|
|
Expression::Template(template)
|
|
|
|
}
|
|
|
|
"if" => {
|
2023-02-11 12:19:15 +00:00
|
|
|
let ([condition_node, true_node], [false_node]) = expect_arguments(function)?;
|
|
|
|
let condition = build_expression(condition_node, build_keyword)?
|
2023-02-05 03:53:03 +00:00
|
|
|
.try_into_boolean()
|
|
|
|
.ok_or_else(|| {
|
2023-02-11 12:19:15 +00:00
|
|
|
TemplateParseError::invalid_argument_type("Boolean", condition_node.span)
|
2023-02-05 03:53:03 +00:00
|
|
|
})?;
|
2023-02-11 12:19:15 +00:00
|
|
|
let true_template = build_expression(true_node, build_keyword)?.into_template();
|
|
|
|
let false_template = false_node
|
|
|
|
.map(|node| build_expression(node, build_keyword))
|
2023-02-05 03:53:03 +00:00
|
|
|
.transpose()?
|
|
|
|
.map(|x| x.into_template());
|
|
|
|
let template = Box::new(ConditionalTemplate::new(
|
|
|
|
condition,
|
|
|
|
true_template,
|
|
|
|
false_template,
|
|
|
|
));
|
|
|
|
Expression::Template(template)
|
|
|
|
}
|
|
|
|
"separate" => {
|
2023-02-11 12:19:15 +00:00
|
|
|
let ([separator_node], content_nodes) = expect_some_arguments(function)?;
|
|
|
|
let separator = build_expression(separator_node, build_keyword)?.into_template();
|
|
|
|
let contents = content_nodes
|
|
|
|
.iter()
|
|
|
|
.map(|node| build_expression(node, build_keyword).map(|x| x.into_template()))
|
2023-02-05 03:53:03 +00:00
|
|
|
.try_collect()?;
|
|
|
|
let template = Box::new(SeparateTemplate::new(separator, contents));
|
|
|
|
Expression::Template(template)
|
|
|
|
}
|
2023-02-11 12:19:15 +00:00
|
|
|
_ => return Err(TemplateParseError::no_such_function(function)),
|
2023-02-05 03:53:03 +00:00
|
|
|
};
|
|
|
|
Ok(expression)
|
|
|
|
}
|
|
|
|
|
2023-02-11 12:19:15 +00:00
|
|
|
fn build_commit_keyword<'a>(
|
2022-02-02 18:14:03 +00:00
|
|
|
repo: RepoRef<'a>,
|
|
|
|
workspace_id: &WorkspaceId,
|
2023-02-11 12:19:15 +00:00
|
|
|
name: &str,
|
|
|
|
span: pest::Span,
|
2023-02-03 10:42:03 +00:00
|
|
|
) -> TemplateParseResult<PropertyAndLabels<'a, Commit>> {
|
2023-01-31 03:48:38 +00:00
|
|
|
fn wrap_fn<'a, O>(
|
|
|
|
f: impl Fn(&Commit) -> O + 'a,
|
|
|
|
) -> Box<dyn TemplateProperty<Commit, Output = O> + 'a> {
|
|
|
|
Box::new(TemplatePropertyFn(f))
|
|
|
|
}
|
2023-02-11 12:19:15 +00:00
|
|
|
let property = match name {
|
2023-01-31 03:48:38 +00:00
|
|
|
"description" => Property::String(wrap_fn(|commit| {
|
|
|
|
cli_util::complete_newline(commit.description())
|
|
|
|
})),
|
|
|
|
"change_id" => Property::CommitOrChangeId(wrap_fn(move |commit| {
|
|
|
|
CommitOrChangeId::new(repo, commit.change_id())
|
|
|
|
})),
|
|
|
|
"commit_id" => Property::CommitOrChangeId(wrap_fn(move |commit| {
|
|
|
|
CommitOrChangeId::new(repo, commit.id())
|
|
|
|
})),
|
|
|
|
"author" => Property::Signature(wrap_fn(|commit| commit.author().clone())),
|
|
|
|
"committer" => Property::Signature(wrap_fn(|commit| commit.committer().clone())),
|
2022-09-18 21:46:12 +00:00
|
|
|
"working_copies" => Property::String(Box::new(WorkingCopiesProperty { repo })),
|
2023-02-03 03:39:48 +00:00
|
|
|
"current_working_copy" => {
|
|
|
|
let workspace_id = workspace_id.clone();
|
|
|
|
Property::Boolean(wrap_fn(move |commit| {
|
|
|
|
Some(commit.id()) == repo.view().get_wc_commit_id(&workspace_id)
|
|
|
|
}))
|
|
|
|
}
|
2021-07-15 08:31:48 +00:00
|
|
|
"branches" => Property::String(Box::new(BranchProperty { repo })),
|
|
|
|
"tags" => Property::String(Box::new(TagProperty { repo })),
|
2021-01-03 08:26:57 +00:00
|
|
|
"git_refs" => Property::String(Box::new(GitRefsProperty { repo })),
|
2022-12-17 18:17:50 +00:00
|
|
|
"git_head" => Property::String(Box::new(GitHeadProperty::new(repo))),
|
2023-01-31 03:48:38 +00:00
|
|
|
"divergent" => Property::Boolean(wrap_fn(move |commit| {
|
|
|
|
// The given commit could be hidden in e.g. obslog.
|
|
|
|
let maybe_entries = repo.resolve_change_id(commit.change_id());
|
|
|
|
maybe_entries.map_or(0, |entries| entries.len()) > 1
|
|
|
|
})),
|
|
|
|
"conflict" => Property::Boolean(wrap_fn(|commit| commit.tree().has_conflict())),
|
|
|
|
"empty" => Property::Boolean(wrap_fn(move |commit| {
|
|
|
|
commit.tree().id() == rewrite::merge_commit_trees(repo, &commit.parents()).id()
|
|
|
|
})),
|
2023-02-11 12:19:15 +00:00
|
|
|
_ => return Err(TemplateParseError::no_such_keyword(name, span)),
|
2020-12-12 08:00:42 +00:00
|
|
|
};
|
2023-02-11 12:19:15 +00:00
|
|
|
Ok(PropertyAndLabels(property, vec![name.to_owned()]))
|
2020-12-12 08:00:42 +00:00
|
|
|
}
|
|
|
|
|
2023-02-11 12:19:15 +00:00
|
|
|
/// Builds template evaluation tree from AST nodes.
|
|
|
|
fn build_expression<'a, C: 'a>(
|
|
|
|
node: &ExpressionNode,
|
|
|
|
build_keyword: &impl Fn(&str, pest::Span) -> TemplateParseResult<PropertyAndLabels<'a, C>>,
|
2023-02-03 13:31:26 +00:00
|
|
|
) -> TemplateParseResult<Expression<'a, C>> {
|
2023-02-11 12:19:15 +00:00
|
|
|
match &node.kind {
|
|
|
|
ExpressionKind::Identifier(name) => {
|
|
|
|
Ok(Expression::Property(build_keyword(name, node.span)?))
|
|
|
|
}
|
|
|
|
ExpressionKind::Integer(value) => {
|
|
|
|
let term = PropertyAndLabels(Property::Integer(Box::new(Literal(*value))), vec![]);
|
|
|
|
Ok(Expression::Property(term))
|
|
|
|
}
|
|
|
|
ExpressionKind::String(value) => {
|
|
|
|
let term =
|
|
|
|
PropertyAndLabels(Property::String(Box::new(Literal(value.clone()))), vec![]);
|
|
|
|
Ok(Expression::Property(term))
|
|
|
|
}
|
|
|
|
ExpressionKind::List(nodes) => {
|
|
|
|
let templates = nodes
|
|
|
|
.iter()
|
|
|
|
.map(|node| build_expression(node, build_keyword).map(|x| x.into_template()))
|
|
|
|
.try_collect()?;
|
|
|
|
Ok(Expression::Template(Box::new(ListTemplate(templates))))
|
2023-02-04 12:10:18 +00:00
|
|
|
}
|
2023-02-11 12:19:15 +00:00
|
|
|
ExpressionKind::FunctionCall(function) => build_global_function(function, build_keyword),
|
|
|
|
ExpressionKind::MethodCall(method) => build_method_call(method, build_keyword),
|
2020-12-12 08:00:42 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-03 13:31:26 +00:00
|
|
|
// TODO: We'll probably need a trait that abstracts the Property enum and
|
|
|
|
// keyword/method parsing functions per the top-level context.
|
|
|
|
pub fn parse_commit_template<'a>(
|
|
|
|
repo: RepoRef<'a>,
|
|
|
|
workspace_id: &WorkspaceId,
|
|
|
|
template_text: &str,
|
2023-02-12 08:53:09 +00:00
|
|
|
_aliases_map: &TemplateAliasesMap,
|
2023-02-03 13:31:26 +00:00
|
|
|
) -> TemplateParseResult<Box<dyn Template<Commit> + 'a>> {
|
2023-02-11 12:19:15 +00:00
|
|
|
let node = parse_template(template_text)?;
|
|
|
|
let expression = build_expression(&node, &|name, span| {
|
|
|
|
build_commit_keyword(repo, workspace_id, name, span)
|
2023-02-05 03:30:17 +00:00
|
|
|
})?;
|
|
|
|
Ok(expression.into_template())
|
2023-02-03 13:31:26 +00:00
|
|
|
}
|
2023-02-04 12:36:11 +00:00
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
fn parse(template_text: &str) -> TemplateParseResult<Expression<()>> {
|
2023-02-11 12:19:15 +00:00
|
|
|
let node = parse_template(template_text)?;
|
|
|
|
build_expression(&node, &|name, span| {
|
|
|
|
Err(TemplateParseError::no_such_keyword(name, span))
|
2023-02-04 12:36:11 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-02-11 11:24:26 +00:00
|
|
|
/// Drops auxiliary data of AST so it can be compared with other node.
|
|
|
|
fn normalize_tree(node: ExpressionNode) -> ExpressionNode {
|
|
|
|
fn empty_span() -> pest::Span<'static> {
|
|
|
|
pest::Span::new("", 0, 0).unwrap()
|
|
|
|
}
|
|
|
|
|
|
|
|
fn normalize_list(nodes: Vec<ExpressionNode>) -> Vec<ExpressionNode> {
|
|
|
|
nodes.into_iter().map(normalize_tree).collect()
|
|
|
|
}
|
|
|
|
|
|
|
|
fn normalize_function_call(function: FunctionCallNode) -> FunctionCallNode {
|
|
|
|
FunctionCallNode {
|
|
|
|
name: function.name,
|
|
|
|
name_span: empty_span(),
|
|
|
|
args: normalize_list(function.args),
|
|
|
|
args_span: empty_span(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let normalized_kind = match node.kind {
|
|
|
|
ExpressionKind::Identifier(_)
|
|
|
|
| ExpressionKind::Integer(_)
|
|
|
|
| ExpressionKind::String(_) => node.kind,
|
|
|
|
ExpressionKind::List(nodes) => ExpressionKind::List(normalize_list(nodes)),
|
|
|
|
ExpressionKind::FunctionCall(function) => {
|
|
|
|
ExpressionKind::FunctionCall(normalize_function_call(function))
|
|
|
|
}
|
|
|
|
ExpressionKind::MethodCall(method) => {
|
|
|
|
let object = Box::new(normalize_tree(*method.object));
|
|
|
|
let function = normalize_function_call(method.function);
|
|
|
|
ExpressionKind::MethodCall(MethodCallNode { object, function })
|
|
|
|
}
|
|
|
|
};
|
|
|
|
ExpressionNode {
|
|
|
|
kind: normalized_kind,
|
|
|
|
span: empty_span(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_parse_tree_eq() {
|
|
|
|
assert_eq!(
|
|
|
|
normalize_tree(parse_template(r#" commit_id.short(1 ) description"#).unwrap()),
|
|
|
|
normalize_tree(parse_template(r#"commit_id.short( 1 ) (description)"#).unwrap()),
|
|
|
|
);
|
|
|
|
assert_ne!(
|
|
|
|
normalize_tree(parse_template(r#" "ab" "#).unwrap()),
|
|
|
|
normalize_tree(parse_template(r#" "a" "b" "#).unwrap()),
|
|
|
|
);
|
|
|
|
assert_ne!(
|
|
|
|
normalize_tree(parse_template(r#" "foo" "0" "#).unwrap()),
|
|
|
|
normalize_tree(parse_template(r#" "foo" 0 "#).unwrap()),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-02-07 09:33:26 +00:00
|
|
|
#[test]
|
|
|
|
fn test_function_call_syntax() {
|
|
|
|
// Trailing comma isn't allowed for empty argument
|
|
|
|
assert!(parse(r#" "".first_line() "#).is_ok());
|
|
|
|
assert!(parse(r#" "".first_line(,) "#).is_err());
|
|
|
|
|
|
|
|
// Trailing comma is allowed for the last argument
|
|
|
|
assert!(parse(r#" "".contains("") "#).is_ok());
|
|
|
|
assert!(parse(r#" "".contains("",) "#).is_ok());
|
|
|
|
assert!(parse(r#" "".contains("" , ) "#).is_ok());
|
|
|
|
assert!(parse(r#" "".contains(,"") "#).is_err());
|
|
|
|
assert!(parse(r#" "".contains("",,) "#).is_err());
|
|
|
|
assert!(parse(r#" "".contains("" , , ) "#).is_err());
|
|
|
|
assert!(parse(r#" label("","") "#).is_ok());
|
|
|
|
assert!(parse(r#" label("","",) "#).is_ok());
|
|
|
|
assert!(parse(r#" label("",,"") "#).is_err());
|
|
|
|
}
|
|
|
|
|
2023-02-04 12:36:11 +00:00
|
|
|
#[test]
|
|
|
|
fn test_integer_literal() {
|
|
|
|
let extract = |x: Expression<()>| x.try_into_integer().unwrap().extract(&());
|
|
|
|
|
|
|
|
assert_eq!(extract(parse("0").unwrap()), 0);
|
|
|
|
assert_eq!(extract(parse("(42)").unwrap()), 42);
|
|
|
|
assert!(parse("00").is_err());
|
|
|
|
|
|
|
|
assert_eq!(extract(parse(&format!("{}", i64::MAX)).unwrap()), i64::MAX);
|
|
|
|
assert!(parse(&format!("{}", (i64::MAX as u64) + 1)).is_err());
|
|
|
|
}
|
2023-02-12 08:21:06 +00:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_parse_alias_decl() {
|
|
|
|
let mut aliases_map = TemplateAliasesMap::new();
|
|
|
|
aliases_map.insert("sym", r#""is symbol""#).unwrap();
|
|
|
|
aliases_map.insert("func(a)", r#""is function""#).unwrap();
|
|
|
|
|
|
|
|
let (id, defn) = aliases_map.get_symbol("sym").unwrap();
|
|
|
|
assert_eq!(id, TemplateAliasId::Symbol("sym"));
|
|
|
|
assert_eq!(defn, r#""is symbol""#);
|
|
|
|
|
|
|
|
let (id, params, defn) = aliases_map.get_function("func").unwrap();
|
|
|
|
assert_eq!(id, TemplateAliasId::Function("func"));
|
|
|
|
assert_eq!(params, ["a"]);
|
|
|
|
assert_eq!(defn, r#""is function""#);
|
|
|
|
|
|
|
|
// Formal parameter 'a' can't be redefined
|
|
|
|
assert_eq!(
|
|
|
|
aliases_map.insert("f(a, a)", r#""""#).unwrap_err().kind,
|
|
|
|
TemplateParseErrorKind::RedefinedFunctionParameter
|
|
|
|
);
|
|
|
|
|
|
|
|
// Trailing comma isn't allowed for empty parameter
|
|
|
|
assert!(aliases_map.insert("f(,)", r#"""#).is_err());
|
|
|
|
// Trailing comma is allowed for the last parameter
|
|
|
|
assert!(aliases_map.insert("g(a,)", r#"""#).is_ok());
|
|
|
|
assert!(aliases_map.insert("h(a , )", r#"""#).is_ok());
|
|
|
|
assert!(aliases_map.insert("i(,a)", r#"""#).is_err());
|
|
|
|
assert!(aliases_map.insert("j(a,,)", r#"""#).is_err());
|
|
|
|
assert!(aliases_map.insert("k(a , , )", r#"""#).is_err());
|
|
|
|
assert!(aliases_map.insert("l(a,b,)", r#"""#).is_ok());
|
|
|
|
assert!(aliases_map.insert("m(a,,b)", r#"""#).is_err());
|
|
|
|
}
|
2023-02-04 12:36:11 +00:00
|
|
|
}
|