relabel.rs 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  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. } else {
  132. return Err(toks.error(ParseError::NoSeparator));
  133. }
  134. if let Some(Token::Word("to")) = toks.peek_token()? {
  135. return Err(toks.error(ParseError::MisleadingTo));
  136. }
  137. // start parsing deltas
  138. let mut deltas = Vec::new();
  139. loop {
  140. deltas.push(LabelDelta::parse(&mut toks)?);
  141. // optional `, and` separator
  142. if let Some(Token::Comma) = toks.peek_token()? {
  143. toks.next_token()?;
  144. }
  145. if let Some(Token::Word("and")) = toks.peek_token()? {
  146. toks.next_token()?;
  147. }
  148. if let Some(Token::Dot) | Some(Token::EndOfLine) = toks.peek_token()? {
  149. toks.next_token()?;
  150. *input = toks;
  151. return Ok(Some(RelabelCommand(deltas)));
  152. }
  153. }
  154. }
  155. }
  156. #[cfg(test)]
  157. fn parse<'a>(input: &'a str) -> Result<Option<Vec<LabelDelta>>, Error<'a>> {
  158. let mut toks = Tokenizer::new(input);
  159. Ok(RelabelCommand::parse(&mut toks)?.map(|c| c.0))
  160. }
  161. #[test]
  162. fn parse_simple() {
  163. assert_eq!(
  164. parse("modify labels: +T-compiler -T-lang bug."),
  165. Ok(Some(vec![
  166. LabelDelta::Add(Label("T-compiler".into())),
  167. LabelDelta::Remove(Label("T-lang".into())),
  168. LabelDelta::Add(Label("bug".into())),
  169. ]))
  170. );
  171. }
  172. #[test]
  173. fn parse_leading_to_label() {
  174. assert_eq!(
  175. parse("modify labels: to -T-lang")
  176. .unwrap_err()
  177. .source()
  178. .unwrap()
  179. .downcast_ref(),
  180. Some(&ParseError::MisleadingTo)
  181. );
  182. }
  183. #[test]
  184. fn parse_no_label_paragraph() {
  185. assert_eq!(
  186. parse("modify labels yep; Labels do in fact exist but this is not a label paragraph.")
  187. .unwrap_err()
  188. .source()
  189. .unwrap()
  190. .downcast_ref(),
  191. Some(&ParseError::NoSeparator)
  192. );
  193. assert_eq!(
  194. parse("Labels do in fact exist but this is not a label paragraph."),
  195. Ok(None),
  196. );
  197. }
  198. #[test]
  199. fn parse_no_dot() {
  200. assert_eq!(
  201. parse("modify labels to +T-compiler -T-lang bug"),
  202. Ok(Some(vec![
  203. LabelDelta::Add(Label("T-compiler".into())),
  204. LabelDelta::Remove(Label("T-lang".into())),
  205. LabelDelta::Add(Label("bug".into())),
  206. ]))
  207. );
  208. }