1use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14
15#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
21pub struct OwnersOutput {
22 pub platform: Option<String>,
25
26 pub path: Option<String>,
28
29 pub header: Option<String>,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
35pub struct OwnerRule {
36 pub pattern: String,
38
39 pub owners: Vec<String>,
41
42 #[serde(skip_serializing_if = "Option::is_none")]
44 pub description: Option<String>,
45
46 #[serde(skip_serializing_if = "Option::is_none")]
48 pub section: Option<String>,
49
50 #[serde(skip_serializing_if = "Option::is_none")]
52 pub order: Option<i32>,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
60#[serde(rename_all = "camelCase")]
61pub struct Owners {
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub output: Option<OwnersOutput>,
65
66 #[serde(default)]
69 pub rules: HashMap<String, OwnerRule>,
70}
71
72impl Owners {
73 #[must_use]
75 pub fn sorted_rules(&self) -> Vec<(&String, &OwnerRule)> {
76 let mut rule_entries: Vec<_> = self.rules.iter().collect();
77 rule_entries.sort_by(|a, b| {
78 let order_a = a.1.order.unwrap_or(i32::MAX);
79 let order_b = b.1.order.unwrap_or(i32::MAX);
80 order_a.cmp(&order_b).then_with(|| a.0.cmp(b.0))
81 });
82 rule_entries
83 }
84
85 #[must_use]
87 pub fn header(&self) -> Option<&str> {
88 self.output.as_ref().and_then(|o| o.header.as_deref())
89 }
90
91 #[must_use]
93 pub fn custom_path(&self) -> Option<&str> {
94 self.output.as_ref().and_then(|o| o.path.as_deref())
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 #[test]
103 fn test_sorted_rules() {
104 let mut rules = HashMap::new();
105 rules.insert(
106 "z-last".to_string(),
107 OwnerRule {
108 pattern: "*.last".to_string(),
109 owners: vec!["@team".to_string()],
110 description: None,
111 section: None,
112 order: Some(3),
113 },
114 );
115 rules.insert(
116 "a-first".to_string(),
117 OwnerRule {
118 pattern: "*.first".to_string(),
119 owners: vec!["@team".to_string()],
120 description: None,
121 section: None,
122 order: Some(1),
123 },
124 );
125 rules.insert(
126 "m-middle".to_string(),
127 OwnerRule {
128 pattern: "*.middle".to_string(),
129 owners: vec!["@team".to_string()],
130 description: None,
131 section: None,
132 order: Some(2),
133 },
134 );
135
136 let owners = Owners {
137 rules,
138 ..Default::default()
139 };
140
141 let sorted = owners.sorted_rules();
142 assert_eq!(sorted.len(), 3);
143 assert_eq!(sorted[0].0, "a-first");
144 assert_eq!(sorted[1].0, "m-middle");
145 assert_eq!(sorted[2].0, "z-last");
146 }
147
148 #[test]
149 fn test_owners_header() {
150 let owners = Owners::default();
152 assert!(owners.header().is_none());
153
154 let owners = Owners {
156 output: Some(OwnersOutput {
157 platform: None,
158 path: None,
159 header: Some("Custom Header".to_string()),
160 }),
161 ..Default::default()
162 };
163 assert_eq!(owners.header(), Some("Custom Header"));
164 }
165
166 #[test]
167 fn test_owners_custom_path() {
168 let owners = Owners::default();
170 assert!(owners.custom_path().is_none());
171
172 let owners = Owners {
174 output: Some(OwnersOutput {
175 platform: None,
176 path: Some("docs/CODEOWNERS".to_string()),
177 header: None,
178 }),
179 ..Default::default()
180 };
181 assert_eq!(owners.custom_path(), Some("docs/CODEOWNERS"));
182 }
183
184 #[test]
185 fn test_owners_output_platform_string() {
186 let owners = Owners {
188 output: Some(OwnersOutput {
189 platform: Some("gitlab".to_string()),
190 path: None,
191 header: None,
192 }),
193 ..Default::default()
194 };
195 assert_eq!(
196 owners.output.as_ref().unwrap().platform,
197 Some("gitlab".to_string())
198 );
199 }
200}