1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
//! Backend API client factory and runtime.
//!
//! Creates and configures an HTTP client for the Ito backend API when
//! backend mode is enabled in the resolved configuration. The client
//! handles authentication, timeouts, and retry logic for transient failures.
use std::path::PathBuf;
use std::time::Duration;
use ito_config::types::BackendApiConfig;
use crate::errors::{CoreError, CoreResult};
/// Resolved backend runtime settings ready for client construction.
///
/// Constructed from [`BackendApiConfig`] with environment variable resolution
/// and validation applied. This type is only created when backend mode is
/// enabled and all required settings are present.
#[derive(Debug, Clone)]
pub struct BackendRuntime {
/// Base URL for the backend API.
pub base_url: String,
/// Resolved bearer token for authentication.
pub token: String,
/// Request timeout.
pub timeout: Duration,
/// Maximum retry attempts for transient failures.
pub max_retries: u32,
/// Directory for artifact backup snapshots.
pub backup_dir: PathBuf,
/// Organization namespace for project-scoped routes.
pub org: String,
/// Repository namespace for project-scoped routes.
pub repo: String,
}
impl BackendRuntime {
/// Returns the project-scoped API path prefix: `/api/v1/projects/{org}/{repo}`.
pub fn project_api_prefix(&self) -> String {
format!(
"{}/api/v1/projects/{}/{}",
self.base_url, self.org, self.repo
)
}
}
/// Resolve backend runtime settings from config.
///
/// Returns `Ok(None)` when backend mode is disabled. Returns `Err` when
/// backend mode is enabled but required values (e.g., token) are missing.
pub fn resolve_backend_runtime(config: &BackendApiConfig) -> CoreResult<Option<BackendRuntime>> {
if !config.enabled {
return Ok(None);
}
let token = resolve_token(config)?;
let backup_dir = resolve_backup_dir(config);
let timeout = Duration::from_millis(config.timeout_ms);
let (org, repo) = resolve_project_namespace(config)?;
Ok(Some(BackendRuntime {
base_url: config.url.clone(),
token,
timeout,
max_retries: config.max_retries,
backup_dir,
org,
repo,
}))
}
/// Resolve the bearer token from explicit config or environment variable.
fn resolve_token(config: &BackendApiConfig) -> CoreResult<String> {
let env_var = &config.token_env_var;
match std::env::var(env_var) {
Ok(val) if !val.trim().is_empty() => return Ok(val.trim().to_string()),
Ok(_) => {
return Err(CoreError::validation(format!(
"Backend mode is enabled but environment variable '{env_var}' is empty. \
Set the token via '{env_var}' or 'backend.token' in config."
)));
}
Err(_) => {}
}
if let Some(token) = &config.token {
let token = token.trim();
if !token.is_empty() {
return Ok(token.to_string());
}
}
Err(CoreError::validation(format!(
"Backend mode is enabled but environment variable '{env_var}' is not set. \
Set the token via '{env_var}' or 'backend.token' in config."
)))
}
/// Resolve the backup directory, falling back to `$HOME/.ito/backups`.
fn resolve_backup_dir(config: &BackendApiConfig) -> PathBuf {
if let Some(dir) = &config.backup_dir {
return PathBuf::from(dir);
}
let home = std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.unwrap_or_else(|_| "/tmp".to_string());
PathBuf::from(home).join(".ito").join("backups")
}
/// Environment variable name for overriding the project organization namespace.
const ENV_PROJECT_ORG: &str = "ITO_BACKEND_PROJECT_ORG";
/// Environment variable name for overriding the project repository namespace.
const ENV_PROJECT_REPO: &str = "ITO_BACKEND_PROJECT_REPO";
/// Resolve the project namespace (org, repo) from env vars with config fallbacks.
///
/// Resolution order for each field:
/// 1. Environment variable (`ITO_BACKEND_PROJECT_ORG` / `ITO_BACKEND_PROJECT_REPO`)
/// 2. Explicit config value (`backend.project.org` / `backend.project.repo`)
///
/// Returns `Err` if either value is missing after fallback resolution.
fn resolve_project_namespace(config: &BackendApiConfig) -> CoreResult<(String, String)> {
resolve_project_namespace_with_env(config, ENV_PROJECT_ORG, ENV_PROJECT_REPO)
}
/// Inner implementation that accepts env var names for testability.
fn resolve_project_namespace_with_env(
config: &BackendApiConfig,
org_env_var: &str,
repo_env_var: &str,
) -> CoreResult<(String, String)> {
let org = std::env::var(org_env_var)
.ok()
.filter(|s| !s.trim().is_empty())
.map(|s| s.trim().to_string())
.or_else(|| {
config
.project
.org
.as_deref()
.filter(|s| !s.is_empty())
.map(String::from)
});
let repo = std::env::var(repo_env_var)
.ok()
.filter(|s| !s.trim().is_empty())
.map(|s| s.trim().to_string())
.or_else(|| {
config
.project
.repo
.as_deref()
.filter(|s| !s.is_empty())
.map(String::from)
});
let Some(org) = org else {
return Err(CoreError::validation(format!(
"Backend mode is enabled but 'backend.project.org' is not set. \
Set it in config or via the {org_env_var} environment variable."
)));
};
let Some(repo) = repo else {
return Err(CoreError::validation(format!(
"Backend mode is enabled but 'backend.project.repo' is not set. \
Set it in config or via the {repo_env_var} environment variable."
)));
};
Ok((org, repo))
}
/// Determines whether a backend error status code is retriable.
///
/// Returns `true` for server errors (5xx) and rate limiting (429).
/// Client errors (4xx other than 429) are not retriable.
pub fn is_retriable_status(status: u16) -> bool {
match status {
429 => true,
s if s >= 500 => true,
_ => false,
}
}
/// Generate a unique idempotency key for a backend operation.
///
/// The key combines a UUID v4 prefix with the operation name for
/// traceability in server logs.
pub fn idempotency_key(operation: &str) -> String {
format!("{}-{operation}", uuid::Uuid::new_v4())
}
#[cfg(test)]
#[path = "backend_client_tests.rs"]
mod backend_client_tests;