opendev_runtime/approval/
types.rs1use regex::Regex;
4use serde::{Deserialize, Serialize};
5use std::sync::OnceLock;
6use tracing::warn;
7
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum RuleAction {
12 AutoApprove,
13 AutoDeny,
14 RequireApproval,
15 RequireEdit,
16}
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(rename_all = "snake_case")]
21pub enum RuleType {
22 Pattern,
24 Command,
26 Prefix,
28 Danger,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct ApprovalRule {
35 pub id: String,
36 pub name: String,
37 pub description: String,
38 pub rule_type: RuleType,
39 pub pattern: String,
40 pub action: RuleAction,
41 #[serde(default = "default_true")]
42 pub enabled: bool,
43 #[serde(default)]
44 pub priority: i32,
45 #[serde(skip_serializing_if = "Option::is_none")]
46 pub created_at: Option<String>,
47 #[serde(skip_serializing_if = "Option::is_none")]
48 pub modified_at: Option<String>,
49
50 #[serde(skip)]
53 pub(crate) compiled_regex: OnceLock<Option<Regex>>,
54}
55
56fn default_true() -> bool {
57 true
58}
59
60impl ApprovalRule {
61 #[allow(clippy::too_many_arguments)]
63 pub fn new(
64 id: String,
65 name: String,
66 description: String,
67 rule_type: RuleType,
68 pattern: String,
69 action: RuleAction,
70 enabled: bool,
71 priority: i32,
72 ) -> Self {
73 Self {
74 id,
75 name,
76 description,
77 rule_type,
78 pattern,
79 action,
80 enabled,
81 priority,
82 created_at: None,
83 modified_at: None,
84 compiled_regex: OnceLock::new(),
85 }
86 }
87
88 fn get_compiled_regex(&self) -> Option<&Regex> {
91 if !matches!(self.rule_type, RuleType::Pattern | RuleType::Danger) {
92 return None;
93 }
94 self.compiled_regex
95 .get_or_init(|| match Regex::new(&self.pattern) {
96 Ok(re) => Some(re),
97 Err(e) => {
98 warn!("Invalid regex pattern '{}': {}", self.pattern, e);
99 None
100 }
101 })
102 .as_ref()
103 }
104
105 pub fn matches(&self, command: &str) -> bool {
107 if !self.enabled {
108 return false;
109 }
110 match self.rule_type {
111 RuleType::Pattern | RuleType::Danger => self
112 .get_compiled_regex()
113 .map(|re| re.is_match(command))
114 .unwrap_or(false),
115 RuleType::Command => command == self.pattern,
116 RuleType::Prefix => {
117 command == self.pattern || command.starts_with(&format!("{} ", self.pattern))
118 }
119 }
120 }
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct CommandHistory {
126 pub command: String,
127 pub approved: bool,
128 #[serde(skip_serializing_if = "Option::is_none")]
129 pub edited_command: Option<String>,
130 #[serde(skip_serializing_if = "Option::is_none")]
131 pub timestamp: Option<String>,
132 #[serde(skip_serializing_if = "Option::is_none")]
133 pub rule_matched: Option<String>,
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub enum RuleScope {
139 User,
140 Project,
141 All,
142}