Skip to main content

governor_application/
owners.rs

1//! Owners use cases.
2
3use governor_core::traits::registry::Registry;
4use governor_owners::{OwnersDiff, ResolvedOwners, resolve_owners, validate_not_empty};
5use serde::Serialize;
6use std::collections::HashMap;
7use std::path::Path;
8
9use crate::error::{ApplicationError, ApplicationResult};
10use crate::ports::{OwnerPackage, OwnersWorkspace, WorkspacePort};
11
12/// Input for showing owners.
13#[derive(Debug, Clone)]
14pub struct OwnersShowInput {
15    /// Workspace root.
16    pub workspace_path: String,
17    /// Include every package.
18    pub all: bool,
19    /// Include owner source explanation.
20    pub explain: bool,
21}
22
23/// Input for checking owners.
24#[derive(Debug, Clone)]
25pub struct OwnersCheckInput {
26    /// Workspace root.
27    pub workspace_path: String,
28    /// Include every package.
29    pub all: bool,
30}
31
32/// Input for syncing owners.
33#[derive(Debug, Clone)]
34pub struct OwnersSyncInput {
35    /// Workspace root.
36    pub workspace_path: String,
37    /// Include every package.
38    pub all: bool,
39    /// Dry run.
40    pub dry_run: bool,
41}
42
43/// Owner entry in output.
44#[derive(Debug, Clone, Serialize)]
45pub struct OwnerEntry {
46    /// Login.
47    pub owner: String,
48    /// Source in merged config.
49    pub source: Option<String>,
50}
51
52#[derive(Debug, Clone, Serialize)]
53pub struct OwnersDiffView {
54    pub to_add: Vec<String>,
55    pub to_remove: Vec<String>,
56}
57
58impl From<&OwnersDiff> for OwnersDiffView {
59    fn from(value: &OwnersDiff) -> Self {
60        Self {
61            to_add: value.to_add.clone(),
62            to_remove: value.to_remove.clone(),
63        }
64    }
65}
66
67/// Owners package report.
68#[derive(Debug, Clone, Serialize)]
69pub struct OwnersPackageReport {
70    /// Crate name.
71    pub name: String,
72    /// Resolved owners.
73    pub owners: Vec<OwnerEntry>,
74    /// Total owner count.
75    pub total: usize,
76    /// Optional drift diff.
77    pub diff: Option<OwnersDiffView>,
78    /// Sync result if relevant.
79    pub sync: Option<OwnersSyncStatus>,
80}
81
82/// Sync status for a package.
83#[derive(Debug, Clone, Serialize)]
84pub struct OwnersSyncStatus {
85    /// Variant name.
86    pub status: String,
87    /// Applied or planned diff.
88    pub diff: Option<OwnersDiffView>,
89    /// Errors keyed by owner.
90    pub errors: HashMap<String, String>,
91}
92
93/// Output for owners show.
94#[derive(Debug, Clone, Serialize)]
95pub struct OwnersShowOutput {
96    /// Workspace root display.
97    pub workspace: String,
98    /// Packages shown.
99    pub packages: Vec<OwnersPackageReport>,
100}
101
102/// Output for owners check.
103#[derive(Debug, Clone, Serialize)]
104pub struct OwnersCheckOutput {
105    /// Workspace root display.
106    pub workspace: String,
107    /// Whether any drift was found.
108    pub drift_detected: bool,
109    /// Per-package reports.
110    pub packages: Vec<OwnersPackageReport>,
111}
112
113/// Output for owners sync.
114#[derive(Debug, Clone, Serialize)]
115pub struct OwnersSyncOutput {
116    /// Workspace root display.
117    pub workspace: String,
118    /// Whether any package had partial failures.
119    pub partial_success: bool,
120    /// Per-package sync status.
121    pub packages: Vec<OwnersPackageReport>,
122}
123
124fn selected_packages(workspace: &OwnersWorkspace, all: bool) -> Vec<&OwnerPackage> {
125    if all {
126        workspace.packages.iter().collect()
127    } else {
128        workspace.packages.first().into_iter().collect()
129    }
130}
131
132fn resolve_package(
133    workspace: &OwnersWorkspace,
134    pkg: &OwnerPackage,
135) -> ApplicationResult<ResolvedOwners> {
136    let workspace_config = workspace.workspace.clone().unwrap_or_default();
137    let package_config = pkg.owners.clone().unwrap_or_default();
138    let resolved = resolve_owners(&workspace_config, &package_config);
139    validate_not_empty(&resolved.owners, &pkg.name)
140        .map_err(|err| ApplicationError::InvalidArguments(err.to_string()))?;
141    Ok(resolved)
142}
143
144fn owner_entries(resolved: &ResolvedOwners, explain: bool) -> Vec<OwnerEntry> {
145    resolved
146        .owners
147        .iter()
148        .map(|owner| OwnerEntry {
149            owner: owner.clone(),
150            source: explain
151                .then(|| resolved.sources.get(owner).cloned())
152                .flatten(),
153        })
154        .collect()
155}
156
157/// Execute owners show.
158///
159/// # Errors
160///
161/// Returns an error when the workspace metadata cannot be loaded or when
162/// the resolved owners configuration is invalid.
163pub async fn show<W: WorkspacePort>(
164    workspace_port: &W,
165    input: OwnersShowInput,
166) -> ApplicationResult<OwnersShowOutput> {
167    let workspace = workspace_port
168        .load_owners_workspace(Path::new(&input.workspace_path))
169        .await?;
170    let reports = selected_packages(&workspace, input.all)
171        .into_iter()
172        .map(|pkg| {
173            let resolved = resolve_package(&workspace, pkg)?;
174            Ok(OwnersPackageReport {
175                name: pkg.name.clone(),
176                owners: owner_entries(&resolved, input.explain),
177                total: resolved.len(),
178                diff: None,
179                sync: None,
180            })
181        })
182        .collect::<ApplicationResult<Vec<_>>>()?;
183
184    Ok(OwnersShowOutput {
185        workspace: workspace.root.display().to_string(),
186        packages: reports,
187    })
188}
189
190/// Execute owners drift check.
191///
192/// # Errors
193///
194/// Returns an error when workspace metadata cannot be loaded or when the
195/// registry lookup fails.
196pub async fn check<W: WorkspacePort, R: Registry>(
197    workspace_port: &W,
198    registry: &R,
199    input: OwnersCheckInput,
200) -> ApplicationResult<OwnersCheckOutput> {
201    let workspace = workspace_port
202        .load_owners_workspace(Path::new(&input.workspace_path))
203        .await?;
204    let mut reports = Vec::new();
205    let mut drift_detected = false;
206
207    for pkg in selected_packages(&workspace, input.all) {
208        let resolved = resolve_package(&workspace, pkg)?;
209        let current = registry
210            .list_owners(&pkg.name)
211            .await?
212            .into_iter()
213            .map(|owner| owner.username)
214            .collect::<Vec<_>>();
215        let diff = OwnersDiff::calculate(&current, &resolved.owners);
216        drift_detected |= !diff.is_empty();
217
218        reports.push(OwnersPackageReport {
219            name: pkg.name.clone(),
220            owners: owner_entries(&resolved, true),
221            total: resolved.len(),
222            diff: Some(OwnersDiffView::from(&diff)),
223            sync: None,
224        });
225    }
226
227    Ok(OwnersCheckOutput {
228        workspace: workspace.root.display().to_string(),
229        drift_detected,
230        packages: reports,
231    })
232}
233
234/// Execute owners sync.
235///
236/// # Errors
237///
238/// Returns an error when workspace metadata cannot be loaded or when the
239/// registry lookup for the current owner set fails.
240pub async fn sync<W: WorkspacePort, R: Registry>(
241    workspace_port: &W,
242    registry: &R,
243    input: OwnersSyncInput,
244) -> ApplicationResult<OwnersSyncOutput> {
245    let workspace = workspace_port
246        .load_owners_workspace(Path::new(&input.workspace_path))
247        .await?;
248    let mut reports = Vec::new();
249    let mut partial_success = false;
250
251    for pkg in selected_packages(&workspace, input.all) {
252        let resolved = resolve_package(&workspace, pkg)?;
253        let current = registry
254            .list_owners(&pkg.name)
255            .await?
256            .into_iter()
257            .map(|owner| owner.username)
258            .collect::<Vec<_>>();
259        let diff = OwnersDiff::calculate(&current, &resolved.owners);
260
261        let sync = if diff.is_empty() {
262            OwnersSyncStatus {
263                status: "already_in_sync".to_string(),
264                diff: None,
265                errors: HashMap::new(),
266            }
267        } else if input.dry_run {
268            OwnersSyncStatus {
269                status: "dry_run".to_string(),
270                diff: Some(OwnersDiffView::from(&diff)),
271                errors: HashMap::new(),
272            }
273        } else {
274            let mut errors = HashMap::new();
275            for owner in &diff.to_add {
276                if let Err(error) = registry.add_owner(&pkg.name, owner).await {
277                    errors.insert(owner.clone(), error.to_string());
278                }
279            }
280            for owner in &diff.to_remove {
281                if let Err(error) = registry.remove_owner(&pkg.name, owner).await {
282                    errors.insert(owner.clone(), error.to_string());
283                }
284            }
285
286            let status = if errors.is_empty() {
287                "success"
288            } else {
289                partial_success = true;
290                "partial"
291            };
292
293            OwnersSyncStatus {
294                status: status.to_string(),
295                diff: Some(OwnersDiffView::from(&diff)),
296                errors,
297            }
298        };
299
300        reports.push(OwnersPackageReport {
301            name: pkg.name.clone(),
302            owners: owner_entries(&resolved, true),
303            total: resolved.len(),
304            diff: Some(OwnersDiffView::from(&diff)),
305            sync: Some(sync),
306        });
307    }
308
309    Ok(OwnersSyncOutput {
310        workspace: workspace.root.display().to_string(),
311        partial_success,
312        packages: reports,
313    })
314}