relabel.rs 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. //! The labels command parser.
  2. //!
  3. //! This can parse arbitrary input, giving the list of labels added/removed.
  4. //!
  5. //! The grammar is as follows:
  6. //!
  7. //! ```text
  8. //! Command: `@bot modify labels:? to? <label-list>.`
  9. //!
  10. //! <label-list>:
  11. //! - <label-delta>
  12. //! - <label-delta> and <label-list>
  13. //! - <label-delta>, <label-list>
  14. //! - <label-delta>, and <label-list>
  15. //!
  16. //! <label-delta>:
  17. //! - +<label>
  18. //! - -<label>
  19. //! this can start with a + or -, but then the only supported way of adding it
  20. //! is with the previous two variants of this (i.e., ++label and -+label).
  21. //! - <label>
  22. //!
  23. //! <label>: \S+
  24. //! ```
  25. use crate::error::Error;
  26. use crate::token::{Token, Tokenizer};
  27. #[cfg(test)]
  28. use std::error::Error as _;
  29. use std::fmt;
  30. #[derive(Debug)]
  31. pub struct RelabelCommand(pub Vec<LabelDelta>);
  32. #[derive(Debug, PartialEq, Eq)]
  33. pub enum LabelDelta {
  34. Add(Label),
  35. Remove(Label),
  36. }
  37. #[derive(Debug, PartialEq, Eq, Clone)]
  38. pub struct Label(String);
  39. #[derive(PartialEq, Eq, Debug)]
  40. pub enum ParseError {
  41. EmptyLabel,
  42. ExpectedLabelDelta,
  43. MisleadingTo,
  44. NoSeparator,
  45. }
  46. impl std::error::Error for ParseError {}
  47. impl fmt::Display for ParseError {
  48. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
  49. match self {
  50. ParseError::EmptyLabel => write!(f, "empty label"),
  51. ParseError::ExpectedLabelDelta => write!(f, "a label delta"),
  52. ParseError::MisleadingTo => write!(f, "forbidden to, use +to"),
  53. ParseError::NoSeparator => write!(f, "must have : or to as label starter"),
  54. }
  55. }
  56. }
  57. impl Label {
  58. fn parse(input: &str) -> Result<Label, ParseError> {
  59. if input.is_empty() {
  60. Err(ParseError::EmptyLabel)
  61. } else {
  62. Ok(Label(input.into()))
  63. }
  64. }
  65. }
  66. impl std::ops::Deref for Label {
  67. type Target = String;
  68. fn deref(&self) -> &String {
  69. &self.0
  70. }
  71. }
  72. impl LabelDelta {
  73. fn parse<'a>(input: &mut Tokenizer<'a>) -> Result<LabelDelta, Error<'a>> {
  74. let delta = match input.peek_token()? {
  75. Some(Token::Word(delta)) => {
  76. input.next_token()?;
  77. delta
  78. }
  79. _ => {
  80. return Err(input.error(ParseError::ExpectedLabelDelta));
  81. }
  82. };
  83. if delta.starts_with('+') {
  84. Ok(LabelDelta::Add(
  85. Label::parse(&delta[1..]).map_err(|e| input.error(e))?,
  86. ))
  87. } else if delta.starts_with('-') {
  88. Ok(LabelDelta::Remove(
  89. Label::parse(&delta[1..]).map_err(|e| input.error(e))?,
  90. ))
  91. } else {
  92. Ok(LabelDelta::Add(
  93. Label::parse(delta).map_err(|e| input.error(e))?,
  94. ))
  95. }
  96. }
  97. pub fn label(&self) -> &Label {
  98. match self {
  99. LabelDelta::Add(l) => l,
  100. LabelDelta::Remove(l) => l,
  101. }
  102. }
  103. }
  104. #[test]
  105. fn delta_empty() {
  106. let mut tok = Tokenizer::new("+ testing");
  107. let err = LabelDelta::parse(&mut tok).unwrap_err();
  108. assert_eq!(
  109. err.source().unwrap().downcast_ref::<ParseError>(),
  110. Some(&ParseError::EmptyLabel)
  111. );
  112. assert_eq!(err.position(), 1);
  113. }
  114. impl RelabelCommand {
  115. pub fn parse<'a>(input: &mut Tokenizer<'a>) -> Result<Option<Self>, Error<'a>> {
  116. let mut toks = input.clone();
  117. if let Some(Token::Word("modify")) = toks.next_token()? {
  118. // continue
  119. } else {
  120. return Ok(None);
  121. }
  122. if let Some(Token::Word("labels")) = toks.next_token()? {
  123. // continue
  124. } else {
  125. return Ok(None);
  126. }
  127. if let Some(Token::Colon) = toks.peek_token()? {
  128. toks.next_token()?;
  129. } else if let Some(Token::Word("to")) = toks.peek_token()? {
  130. toks.next_token()?;
  131. // optionally eat the colon after to, e.g.:
  132. // @rustbot modify labels to: -S-waiting-on-author, +S-waiting-on-review
  133. if let Ok(Some(Token::Colon)) = toks.peek_token() {
  134. toks.next_token()?;
  135. }
  136. } else {
  137. return Err(toks.error(ParseError::NoSeparator));
  138. }
  139. if let Some(Token::Word("to")) = toks.peek_token()? {
  140. return Err(toks.error(ParseError::MisleadingTo));
  141. }
  142. // start parsing deltas
  143. let mut deltas = Vec::new();
  144. loop {
  145. deltas.push(LabelDelta::parse(&mut toks)?);
  146. // optional `, and` separator
  147. if let Some(Token::Comma) = toks.peek_token()? {
  148. toks.next_token()?;
  149. }
  150. if let Some(Token::Word("and")) = toks.peek_token()? {
  151. toks.next_token()?;
  152. }
  153. if let Some(Token::Dot) | Some(Token::EndOfLine) = toks.peek_token()? {
  154. toks.next_token()?;
  155. *input = toks;
  156. return Ok(Some(RelabelCommand(deltas)));
  157. }
  158. }
  159. }
  160. }
  161. #[cfg(test)]
  162. fn parse<'a>(input: &'a str) -> Result<Option<Vec<LabelDelta>>, Error<'a>> {
  163. let mut toks = Tokenizer::new(input);
  164. Ok(RelabelCommand::parse(&mut toks)?.map(|c| c.0))
  165. }
  166. #[test]
  167. fn parse_simple() {
  168. assert_eq!(
  169. parse("modify labels: +T-compiler -T-lang bug."),
  170. Ok(Some(vec![
  171. LabelDelta::Add(Label("T-compiler".into())),
  172. LabelDelta::Remove(Label("T-lang".into())),
  173. LabelDelta::Add(Label("bug".into())),
  174. ]))
  175. );
  176. }
  177. #[test]
  178. fn parse_leading_to_label() {
  179. assert_eq!(
  180. parse("modify labels: to -T-lang")
  181. .unwrap_err()
  182. .source()
  183. .unwrap()
  184. .downcast_ref(),
  185. Some(&ParseError::MisleadingTo)
  186. );
  187. }
  188. #[test]
  189. fn parse_no_label_paragraph() {
  190. assert_eq!(
  191. parse("modify labels yep; Labels do in fact exist but this is not a label paragraph.")
  192. .unwrap_err()
  193. .source()
  194. .unwrap()
  195. .downcast_ref(),
  196. Some(&ParseError::NoSeparator)
  197. );
  198. assert_eq!(
  199. parse("Labels do in fact exist but this is not a label paragraph."),
  200. Ok(None),
  201. );
  202. }
  203. #[test]
  204. fn parse_no_dot() {
  205. assert_eq!(
  206. parse("modify labels to +T-compiler -T-lang bug"),
  207. Ok(Some(vec![
  208. LabelDelta::Add(Label("T-compiler".into())),
  209. LabelDelta::Remove(Label("T-lang".into())),
  210. LabelDelta::Add(Label("bug".into())),
  211. ]))
  212. );
  213. }
  214. #[test]
  215. fn parse_to_colon() {
  216. assert_eq!(
  217. parse("modify labels to: +T-compiler -T-lang bug"),
  218. Ok(Some(vec![
  219. LabelDelta::Add(Label("T-compiler".into())),
  220. LabelDelta::Remove(Label("T-lang".into())),
  221. LabelDelta::Add(Label("bug".into())),
  222. ]))
  223. );
  224. }