Skip to main content

ito_core/
backend_coordination.rs

1//! Backend coordination use-cases for CLI commands.
2//!
3//! Provides the business logic for claim, release, allocate, and sync
4//! operations that the CLI adapter calls. Each function accepts trait
5//! objects for the backend clients so the CLI can inject the concrete
6//! implementation.
7
8use std::path::Path;
9
10use ito_domain::backend::{
11    AllocateResult, ArchiveResult, ArtifactBundle, BackendArchiveClient, BackendLeaseClient,
12    BackendSyncClient, ClaimResult, PushResult, ReleaseResult,
13};
14
15use crate::backend_sync::map_backend_error;
16use crate::errors::{CoreError, CoreResult};
17
18/// Claim a change lease through the backend.
19pub fn claim_change(
20    lease_client: &dyn BackendLeaseClient,
21    change_id: &str,
22) -> CoreResult<ClaimResult> {
23    lease_client
24        .claim(change_id)
25        .map_err(|e| map_backend_error(e, "claim"))
26}
27
28/// Release a change lease through the backend.
29pub fn release_change(
30    lease_client: &dyn BackendLeaseClient,
31    change_id: &str,
32) -> CoreResult<ReleaseResult> {
33    lease_client
34        .release(change_id)
35        .map_err(|e| map_backend_error(e, "release"))
36}
37
38/// Allocate the next available change through the backend.
39pub fn allocate_change(lease_client: &dyn BackendLeaseClient) -> CoreResult<AllocateResult> {
40    lease_client
41        .allocate()
42        .map_err(|e| map_backend_error(e, "allocate"))
43}
44
45/// Pull artifacts from the backend for a change.
46pub fn sync_pull(
47    sync_client: &dyn BackendSyncClient,
48    ito_path: &std::path::Path,
49    change_id: &str,
50    backup_dir: &std::path::Path,
51) -> CoreResult<ArtifactBundle> {
52    crate::backend_sync::pull_artifacts(sync_client, ito_path, change_id, backup_dir)
53}
54
55/// Push local artifacts to the backend for a change.
56pub fn sync_push(
57    sync_client: &dyn BackendSyncClient,
58    ito_path: &std::path::Path,
59    change_id: &str,
60    backup_dir: &std::path::Path,
61) -> CoreResult<PushResult> {
62    crate::backend_sync::push_artifacts(sync_client, ito_path, change_id, backup_dir)
63}
64
65/// Result of a backend-mode archive orchestration.
66#[derive(Debug)]
67pub struct BackendArchiveOutcome {
68    /// Spec IDs that were copied to the main specs tree.
69    pub specs_updated: Vec<String>,
70    /// The archive folder name (date-prefixed).
71    pub archive_name: String,
72    /// Backend archive result with timestamp.
73    pub backend_result: ArchiveResult,
74}
75
76/// Orchestrate a backend-mode archive for a change.
77///
78/// The flow is:
79/// 1. Pull the final artifact bundle from the backend.
80/// 2. Copy spec deltas to the main specs tree (unless `skip_specs`).
81/// 3. Move the change to the archive directory.
82/// 4. Mark the change as archived on the backend.
83///
84/// If step 4 fails, the local archive is already committed — the caller
85/// should report the backend error but NOT roll back the local archive
86/// (the local state is correct; the backend can be retried).
87pub fn archive_with_backend(
88    sync_client: &dyn BackendSyncClient,
89    archive_client: &dyn BackendArchiveClient,
90    ito_path: &Path,
91    change_id: &str,
92    backup_dir: &Path,
93    skip_specs: bool,
94) -> CoreResult<BackendArchiveOutcome> {
95    // Step 1: Pull final artifacts from backend
96    crate::backend_sync::pull_artifacts(sync_client, ito_path, change_id, backup_dir)?;
97
98    // Step 2: Copy spec deltas to main specs tree
99    let specs_updated = if skip_specs {
100        Vec::new()
101    } else {
102        let spec_names = crate::archive::discover_change_specs(ito_path, change_id)?;
103        crate::archive::copy_specs_to_main(ito_path, change_id, &spec_names)?
104    };
105
106    // Step 3: Move to archive
107    let archive_name = crate::archive::generate_archive_name(change_id);
108    crate::archive::move_to_archive(ito_path, change_id, &archive_name)?;
109
110    // Step 4: Mark archived on backend
111    let backend_result = archive_client
112        .mark_archived(change_id)
113        .map_err(|e| map_backend_error(e, "archive"))?;
114
115    Ok(BackendArchiveOutcome {
116        specs_updated,
117        archive_name,
118        backend_result,
119    })
120}
121
122/// Check whether the given `CoreError` represents a backend availability failure.
123///
124/// The CLI can use this to suggest fallback to filesystem mode.
125pub fn is_backend_unavailable(err: &CoreError) -> bool {
126    match err {
127        CoreError::Process(msg) => msg.contains("Backend unavailable"),
128        _ => false,
129    }
130}
131
132#[cfg(test)]
133#[path = "backend_coordination_tests.rs"]
134mod backend_coordination_tests;