use crate::error::{BzrError, Result};
use crate::types::{FlagStatus, FlagUpdate};
pub fn parse_flags(raw: &[String]) -> Result<Vec<FlagUpdate>> {
let mut flags = Vec::new();
for s in raw {
let (name, status, requestee) = parse_single_flag(s)?;
flags.push(FlagUpdate {
name,
status,
requestee,
});
}
Ok(flags)
}
fn parse_single_flag(s: &str) -> Result<(String, FlagStatus, Option<String>)> {
let status_pos = s.find(['+', '-', '?', 'X']).ok_or_else(|| {
BzrError::InputValidation(format!(
"invalid flag '{s}': must contain +, -, ?, or X (e.g. 'review?')"
))
})?;
let name = s[..status_pos].to_string();
if name.is_empty() {
return Err(BzrError::InputValidation(format!(
"invalid flag '{s}': flag name cannot be empty"
)));
}
let status = match s.as_bytes()[status_pos] {
b'+' => FlagStatus::Grant,
b'-' => FlagStatus::Deny,
b'?' => FlagStatus::Request,
b'X' => FlagStatus::Clear,
_ => unreachable!("find() only matches +, -, ?, X"),
};
let remainder = &s[status_pos + 1..];
let requestee = if remainder.starts_with('(') && remainder.ends_with(')') {
Some(remainder[1..remainder.len() - 1].to_string())
} else if remainder.is_empty() {
None
} else {
return Err(BzrError::InputValidation(format!(
"invalid flag '{s}': requestee must be in parentheses"
)));
};
Ok((name, status, requestee))
}
#[cfg(test)]
#[expect(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn parse_flag_with_request() {
let flags = parse_flags(&["review?(alice@example.com)".into()]).unwrap();
assert_eq!(flags.len(), 1);
assert_eq!(flags[0].name, "review");
assert_eq!(flags[0].status, FlagStatus::Request);
assert_eq!(flags[0].requestee.as_deref(), Some("alice@example.com"));
}
#[test]
fn parse_flag_grant() {
let flags = parse_flags(&["review+".into()]).unwrap();
assert_eq!(flags[0].name, "review");
assert_eq!(flags[0].status, FlagStatus::Grant);
assert!(flags[0].requestee.is_none());
}
#[test]
fn parse_flag_deny() {
let flags = parse_flags(&["review-".into()]).unwrap();
assert_eq!(flags[0].status, FlagStatus::Deny);
}
#[test]
fn parse_flag_no_status_char_fails() {
let err = parse_flags(&["review".into()]).unwrap_err();
assert!(err.to_string().contains("must contain"));
}
#[test]
fn parse_flag_empty_name_fails() {
let err = parse_flags(&["?".into()]).unwrap_err();
assert!(err.to_string().contains("cannot be empty"));
}
#[test]
fn parse_flag_bad_requestee_fails() {
let err = parse_flags(&["review?alice".into()]).unwrap_err();
assert!(err.to_string().contains("parentheses"));
}
#[test]
fn parse_flag_unclosed_paren_fails() {
let err = parse_flags(&["review?(alice".into()]).unwrap_err();
assert!(err.to_string().contains("parentheses"));
}
#[test]
fn parse_flag_unopened_paren_fails() {
let err = parse_flags(&["review?alice)".into()]).unwrap_err();
assert!(err.to_string().contains("parentheses"));
}
#[test]
fn parse_flag_clear() {
let flags = parse_flags(&["reviewX".into()]).unwrap();
assert_eq!(flags[0].name, "review");
assert_eq!(flags[0].status, FlagStatus::Clear);
assert!(flags[0].requestee.is_none());
}
#[test]
fn parse_multiple_flags() {
let flags = parse_flags(&["review+".into(), "approval?".into()]).unwrap();
assert_eq!(flags.len(), 2);
assert_eq!(flags[0].name, "review");
assert_eq!(flags[0].status, FlagStatus::Grant);
assert_eq!(flags[1].name, "approval");
assert_eq!(flags[1].status, FlagStatus::Request);
}
#[test]
fn parse_empty_flags() {
let flags = parse_flags(&[]).unwrap();
assert!(flags.is_empty());
}
}