1use std::path::Path;
2
3use once_cell::sync::Lazy;
4use regex::Regex;
5
6use crate::patternset;
7
8#[derive(Clone)]
21pub struct RuleSet {
22 rules: Vec<Rule>,
23 matcher: patternset::Matcher,
24}
25
26impl RuleSet {
27 pub fn new(rules: Vec<Rule>) -> Self {
29 let mut builder = patternset::Builder::new();
30 for rule in &rules {
31 builder.add(&rule.pattern);
32 }
33 let matcher = builder.build();
34 Self { rules, matcher }
35 }
36
37 pub fn matching_rule(&self, path: impl AsRef<Path>) -> Option<&Rule> {
41 self.matcher
42 .matching_patterns(path)
43 .iter()
44 .max()
45 .map(|&idx| &self.rules[idx])
46 }
47
48 pub fn owners(&self, path: impl AsRef<Path>) -> Option<&[Owner]> {
51 return self.matching_rule(path).and_then(|rule| {
52 if rule.owners.is_empty() {
53 None
54 } else {
55 Some(rule.owners.as_ref())
56 }
57 });
58 }
59
60 pub fn all_matching_rules(&self, path: impl AsRef<Path>) -> Vec<(usize, &Rule)> {
64 self.matcher
65 .matching_patterns(path)
66 .iter()
67 .map(|&idx| (idx, &self.rules[idx]))
68 .collect()
69 }
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
75pub struct Rule {
76 pub pattern: String,
77 pub owners: Vec<Owner>,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct Owner {
82 pub value: String,
83 pub kind: OwnerKind,
84}
85
86impl Owner {
87 pub fn new(value: String, kind: OwnerKind) -> Self {
88 Self { value, kind }
89 }
90}
91
92static EMAIL_REGEX: Lazy<Regex> =
93 Lazy::new(|| Regex::new(r"\A[A-Z0-9a-z\._'%\+\-]+@[A-Za-z0-9\.\-]+\.[A-Za-z]{2,6}\z").unwrap());
94static USERNAME_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\A@[a-zA-Z0-9\-_]+\z").unwrap());
95static TEAM_REGEX: Lazy<Regex> =
96 Lazy::new(|| Regex::new(r"\A@[a-zA-Z0-9\-]+/[a-zA-Z0-9\-_]+\z").unwrap());
97
98#[derive(Debug, Clone)]
99pub struct InvalidOwnerError {
100 value: String,
101}
102
103impl std::fmt::Display for InvalidOwnerError {
104 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 write!(f, "invalid owner: {}", self.value)
106 }
107}
108
109impl std::error::Error for InvalidOwnerError {}
110
111impl TryFrom<String> for Owner {
112 type Error = InvalidOwnerError;
113
114 fn try_from(value: String) -> Result<Self, Self::Error> {
115 if EMAIL_REGEX.is_match(&value) {
116 Ok(Self::new(value, OwnerKind::Email))
117 } else if USERNAME_REGEX.is_match(&value) {
118 Ok(Self::new(value, OwnerKind::User))
119 } else if TEAM_REGEX.is_match(&value) {
120 Ok(Self::new(value, OwnerKind::Team))
121 } else {
122 Err(InvalidOwnerError { value })
123 }
124 }
125}
126
127#[derive(Debug, Clone, PartialEq, Eq)]
128pub enum OwnerKind {
129 User,
130 Team,
131 Email,
132}