1use std::path::{Path, PathBuf};
8use std::sync::Arc;
9
10use ito_config::ito_dir::{absolutize_and_normalize, lexical_normalize};
11use ito_config::types::{ItoConfig, RepositoryPersistenceMode};
12use ito_config::{ConfigContext, load_cascading_project_config};
13
14use crate::backend_change_repository::BackendChangeRepository;
15use crate::backend_client::{BackendRuntime, resolve_backend_runtime};
16use crate::backend_http::BackendHttpClient;
17use crate::backend_module_repository::BackendModuleRepository;
18use crate::backend_spec_repository::BackendSpecRepository;
19use crate::change_repository::FsChangeRepository;
20use crate::errors::{CoreError, CoreResult};
21use crate::module_repository::FsModuleRepository;
22use crate::remote_task_repository::RemoteTaskRepository;
23use crate::spec_repository::FsSpecRepository;
24use crate::sqlite_project_store::SqliteBackendProjectStore;
25use crate::task_mutations::{FsTaskMutationService, boxed_fs_task_mutation_service};
26use crate::task_repository::FsTaskRepository;
27use ito_domain::changes::ChangeRepository;
28use ito_domain::modules::ModuleRepository;
29use ito_domain::specs::SpecRepository;
30use ito_domain::tasks::{TaskMutationService, TaskRepository};
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum PersistenceMode {
35 Filesystem,
37 Sqlite,
39 Remote,
41}
42
43#[derive(Clone)]
45pub struct RepositorySet {
46 pub changes: Arc<dyn ChangeRepository + Send + Sync>,
48 pub modules: Arc<dyn ModuleRepository + Send + Sync>,
50 pub tasks: Arc<dyn TaskRepository + Send + Sync>,
52 pub task_mutations: Arc<dyn TaskMutationService + Send + Sync>,
54 pub specs: Arc<dyn SpecRepository + Send + Sync>,
56}
57
58#[derive(Debug, Clone)]
60pub struct SqliteRuntime {
61 pub db_path: PathBuf,
63 pub org: String,
65 pub repo: String,
67}
68
69pub struct RepositoryRuntime {
71 mode: PersistenceMode,
72 ito_path: PathBuf,
73 backend_runtime: Option<BackendRuntime>,
74 sqlite_runtime: Option<SqliteRuntime>,
75 repositories: RepositorySet,
76}
77
78impl RepositoryRuntime {
79 pub fn mode(&self) -> PersistenceMode {
81 self.mode
82 }
83
84 pub fn ito_path(&self) -> &Path {
86 self.ito_path.as_path()
87 }
88
89 pub fn backend_runtime(&self) -> Option<&BackendRuntime> {
91 self.backend_runtime.as_ref()
92 }
93
94 pub fn sqlite_runtime(&self) -> Option<&SqliteRuntime> {
96 self.sqlite_runtime.as_ref()
97 }
98
99 pub fn repositories(&self) -> &RepositorySet {
101 &self.repositories
102 }
103}
104
105pub trait RemoteRepositoryFactory: Send + Sync {
107 fn build(&self, runtime: &BackendRuntime) -> CoreResult<RepositorySet>;
109}
110
111pub struct HttpRemoteRepositoryFactory;
113
114impl RemoteRepositoryFactory for HttpRemoteRepositoryFactory {
115 fn build(&self, runtime: &BackendRuntime) -> CoreResult<RepositorySet> {
116 let client = BackendHttpClient::new(runtime.clone());
117 Ok(RepositorySet {
118 changes: Arc::new(BackendChangeRepository::new(client.clone())),
119 modules: Arc::new(BackendModuleRepository::new(client.clone())),
120 tasks: Arc::new(RemoteTaskRepository::new(client.clone())),
121 task_mutations: Arc::new(client.clone()),
122 specs: Arc::new(BackendSpecRepository::new(client.clone())),
123 })
124 }
125}
126
127pub struct RepositoryRuntimeBuilder {
129 ito_path: PathBuf,
130 mode: PersistenceMode,
131 backend_runtime: Option<BackendRuntime>,
132 sqlite_runtime: Option<SqliteRuntime>,
133 remote_factory: Arc<dyn RemoteRepositoryFactory>,
134}
135
136impl RepositoryRuntimeBuilder {
137 pub fn new(ito_path: impl Into<PathBuf>) -> Self {
139 Self {
140 ito_path: ito_path.into(),
141 mode: PersistenceMode::Filesystem,
142 backend_runtime: None,
143 sqlite_runtime: None,
144 remote_factory: Arc::new(HttpRemoteRepositoryFactory),
145 }
146 }
147
148 pub fn mode(mut self, mode: PersistenceMode) -> Self {
150 self.mode = mode;
151 self
152 }
153
154 pub fn backend_runtime(mut self, runtime: BackendRuntime) -> Self {
156 self.backend_runtime = Some(runtime);
157 self
158 }
159
160 pub fn sqlite_runtime(mut self, runtime: SqliteRuntime) -> Self {
162 self.sqlite_runtime = Some(runtime);
163 self
164 }
165
166 pub fn remote_factory(mut self, factory: Arc<dyn RemoteRepositoryFactory>) -> Self {
168 self.remote_factory = factory;
169 self
170 }
171
172 pub fn build(self) -> CoreResult<RepositoryRuntime> {
174 match self.mode {
175 PersistenceMode::Filesystem => {
176 let repositories = filesystem_repository_set(&self.ito_path);
177 Ok(RepositoryRuntime {
178 mode: PersistenceMode::Filesystem,
179 ito_path: self.ito_path,
180 backend_runtime: None,
181 sqlite_runtime: None,
182 repositories,
183 })
184 }
185 PersistenceMode::Sqlite => {
186 let runtime = self.sqlite_runtime.ok_or_else(|| {
187 CoreError::validation("sqlite mode requires sqlite runtime".to_string())
188 })?;
189 let repositories = sqlite_repository_set(&runtime)?;
190 Ok(RepositoryRuntime {
191 mode: PersistenceMode::Sqlite,
192 ito_path: self.ito_path,
193 backend_runtime: None,
194 sqlite_runtime: Some(runtime),
195 repositories,
196 })
197 }
198 PersistenceMode::Remote => {
199 let runtime = self.backend_runtime.ok_or_else(|| {
200 CoreError::validation("remote mode requires backend runtime".to_string())
201 })?;
202 let repositories = self.remote_factory.build(&runtime)?;
203 Ok(RepositoryRuntime {
204 mode: PersistenceMode::Remote,
205 ito_path: self.ito_path,
206 backend_runtime: Some(runtime),
207 sqlite_runtime: None,
208 repositories,
209 })
210 }
211 }
212 }
213}
214
215pub fn resolve_repository_runtime(
217 ito_path: &Path,
218 ctx: &ConfigContext,
219) -> CoreResult<RepositoryRuntime> {
220 let project_root = ctx
221 .project_dir
222 .as_deref()
223 .unwrap_or_else(|| ito_path.parent().unwrap_or(ito_path));
224 let merged = load_cascading_project_config(project_root, ito_path, ctx).merged;
225 let backend_enabled = merged
226 .pointer("/backend/enabled")
227 .and_then(|v| v.as_bool())
228 .unwrap_or(false);
229 let raw_mode = merged
230 .pointer("/repository/mode")
231 .and_then(|v| v.as_str())
232 .unwrap_or("filesystem");
233
234 if RepositoryPersistenceMode::parse_value(raw_mode).is_none() {
238 let valid = RepositoryPersistenceMode::ALL.join(", ");
239 return Err(CoreError::validation(format!(
240 "Invalid repository.mode '{raw_mode}': must be one of {valid}"
241 )));
242 }
243
244 let sqlite_enabled = raw_mode == "sqlite";
245
246 let config = match serde_json::from_value::<ItoConfig>(merged) {
247 Ok(config) => config,
248 Err(err) => {
249 if backend_enabled || sqlite_enabled {
250 let mode = if backend_enabled {
251 "backend mode is enabled"
252 } else {
253 "sqlite persistence mode is enabled"
254 };
255 return Err(CoreError::validation(format!(
256 "Failed to parse Ito config while {mode}: {err}"
257 )));
258 }
259 return RepositoryRuntimeBuilder::new(ito_path).build();
260 }
261 };
262
263 if !config.backend.enabled {
264 return match config.repository.mode {
265 RepositoryPersistenceMode::Filesystem => {
266 RepositoryRuntimeBuilder::new(ito_path).build()
267 }
268 RepositoryPersistenceMode::Sqlite => {
269 let runtime = resolve_sqlite_runtime(&config, project_root)?;
270 RepositoryRuntimeBuilder::new(ito_path)
271 .mode(PersistenceMode::Sqlite)
272 .sqlite_runtime(runtime)
273 .build()
274 }
275 };
276 }
277
278 let runtime = resolve_backend_runtime(&config.backend)?.ok_or_else(|| {
279 CoreError::validation("Backend mode is enabled but runtime was not resolved".to_string())
280 })?;
281
282 RepositoryRuntimeBuilder::new(ito_path)
283 .mode(PersistenceMode::Remote)
284 .backend_runtime(runtime)
285 .build()
286}
287
288fn resolve_sqlite_runtime(config: &ItoConfig, project_root: &Path) -> CoreResult<SqliteRuntime> {
289 let Some(db_path) = config.repository.sqlite.db_path.as_deref() else {
290 return Err(CoreError::validation(
291 "SQLite persistence mode requires 'repository.sqlite.dbPath' to be set",
292 ));
293 };
294 let db_path = db_path.trim();
295 if db_path.is_empty() {
296 return Err(CoreError::validation(
297 "SQLite persistence mode requires 'repository.sqlite.dbPath' to be set",
298 ));
299 }
300
301 let db_path = PathBuf::from(db_path);
302 let db_path = if db_path.is_absolute() {
303 db_path
304 } else {
305 project_root.join(db_path)
306 };
307 let db_path =
308 absolutize_and_normalize(&db_path).unwrap_or_else(|_| lexical_normalize(&db_path));
309
310 let repo = match project_root.file_name().and_then(|s| s.to_str()) {
311 Some(name) if !name.trim().is_empty() => name.to_string(),
312 _ => "project".to_string(),
313 };
314
315 Ok(SqliteRuntime {
316 db_path,
317 org: "local".to_string(),
318 repo,
319 })
320}
321
322fn filesystem_repository_set(ito_path: &Path) -> RepositorySet {
323 let ito_path = ito_path.to_path_buf();
324 RepositorySet {
325 changes: Arc::new(OwnedFsChangeRepository::new(ito_path.clone())),
326 modules: Arc::new(OwnedFsModuleRepository::new(ito_path.clone())),
327 tasks: Arc::new(OwnedFsTaskRepository::new(ito_path.clone())),
328 task_mutations: Arc::new(FsTaskMutationService::new(ito_path.clone())),
329 specs: Arc::new(OwnedFsSpecRepository::new(ito_path)),
330 }
331}
332
333fn sqlite_repository_set(runtime: &SqliteRuntime) -> CoreResult<RepositorySet> {
334 let store = SqliteBackendProjectStore::open(&runtime.db_path)?;
335 store.repository_set(&runtime.org, &runtime.repo)
336}
337
338pub(crate) fn boxed_fs_change_repository(ito_path: PathBuf) -> Box<dyn ChangeRepository + Send> {
339 Box::new(OwnedFsChangeRepository::new(ito_path))
340}
341
342pub(crate) fn boxed_fs_module_repository(ito_path: PathBuf) -> Box<dyn ModuleRepository + Send> {
343 Box::new(OwnedFsModuleRepository::new(ito_path))
344}
345
346pub(crate) fn boxed_fs_task_repository(ito_path: PathBuf) -> Box<dyn TaskRepository + Send> {
347 Box::new(OwnedFsTaskRepository::new(ito_path))
348}
349
350pub(crate) fn boxed_fs_task_mutation_port(
351 ito_path: PathBuf,
352) -> Box<dyn TaskMutationService + Send> {
353 boxed_fs_task_mutation_service(ito_path)
354}
355
356pub(crate) fn boxed_fs_spec_repository(ito_path: PathBuf) -> Box<dyn SpecRepository + Send> {
357 Box::new(OwnedFsSpecRepository::new(ito_path))
358}
359
360#[derive(Debug, Clone)]
363struct OwnedFsChangeRepository {
364 ito_path: PathBuf,
365}
366
367impl OwnedFsChangeRepository {
368 fn new(ito_path: PathBuf) -> Self {
369 Self { ito_path }
370 }
371
372 fn inner(&self) -> FsChangeRepository<'_> {
373 FsChangeRepository::new(&self.ito_path)
374 }
375}
376
377impl ChangeRepository for OwnedFsChangeRepository {
378 fn resolve_target_with_options(
379 &self,
380 input: &str,
381 options: ito_domain::changes::ResolveTargetOptions,
382 ) -> ito_domain::changes::ChangeTargetResolution {
383 self.inner().resolve_target_with_options(input, options)
384 }
385
386 fn suggest_targets(&self, input: &str, max: usize) -> Vec<String> {
387 self.inner().suggest_targets(input, max)
388 }
389
390 fn exists(&self, id: &str) -> bool {
391 self.inner().exists(id)
392 }
393
394 fn exists_with_filter(
395 &self,
396 id: &str,
397 filter: ito_domain::changes::ChangeLifecycleFilter,
398 ) -> bool {
399 self.inner().exists_with_filter(id, filter)
400 }
401
402 fn get_with_filter(
403 &self,
404 id: &str,
405 filter: ito_domain::changes::ChangeLifecycleFilter,
406 ) -> ito_domain::errors::DomainResult<ito_domain::changes::Change> {
407 self.inner().get_with_filter(id, filter)
408 }
409
410 fn list_with_filter(
411 &self,
412 filter: ito_domain::changes::ChangeLifecycleFilter,
413 ) -> ito_domain::errors::DomainResult<Vec<ito_domain::changes::ChangeSummary>> {
414 self.inner().list_with_filter(filter)
415 }
416
417 fn list_by_module_with_filter(
418 &self,
419 module_id: &str,
420 filter: ito_domain::changes::ChangeLifecycleFilter,
421 ) -> ito_domain::errors::DomainResult<Vec<ito_domain::changes::ChangeSummary>> {
422 self.inner().list_by_module_with_filter(module_id, filter)
423 }
424
425 fn list_incomplete_with_filter(
426 &self,
427 filter: ito_domain::changes::ChangeLifecycleFilter,
428 ) -> ito_domain::errors::DomainResult<Vec<ito_domain::changes::ChangeSummary>> {
429 self.inner().list_incomplete_with_filter(filter)
430 }
431
432 fn list_complete_with_filter(
433 &self,
434 filter: ito_domain::changes::ChangeLifecycleFilter,
435 ) -> ito_domain::errors::DomainResult<Vec<ito_domain::changes::ChangeSummary>> {
436 self.inner().list_complete_with_filter(filter)
437 }
438
439 fn get_summary_with_filter(
440 &self,
441 id: &str,
442 filter: ito_domain::changes::ChangeLifecycleFilter,
443 ) -> ito_domain::errors::DomainResult<ito_domain::changes::ChangeSummary> {
444 self.inner().get_summary_with_filter(id, filter)
445 }
446}
447
448#[derive(Debug, Clone)]
449struct OwnedFsModuleRepository {
450 ito_path: PathBuf,
451}
452
453impl OwnedFsModuleRepository {
454 fn new(ito_path: PathBuf) -> Self {
455 Self { ito_path }
456 }
457
458 fn inner(&self) -> FsModuleRepository<'_> {
459 FsModuleRepository::new(&self.ito_path)
460 }
461}
462
463impl ModuleRepository for OwnedFsModuleRepository {
464 fn exists(&self, id: &str) -> bool {
465 self.inner().exists(id)
466 }
467
468 fn get(
469 &self,
470 id_or_name: &str,
471 ) -> ito_domain::errors::DomainResult<ito_domain::modules::Module> {
472 self.inner().get(id_or_name)
473 }
474
475 fn list(&self) -> ito_domain::errors::DomainResult<Vec<ito_domain::modules::ModuleSummary>> {
476 self.inner().list()
477 }
478}
479
480#[derive(Debug, Clone)]
481struct OwnedFsTaskRepository {
482 ito_path: PathBuf,
483}
484
485impl OwnedFsTaskRepository {
486 fn new(ito_path: PathBuf) -> Self {
487 Self { ito_path }
488 }
489
490 fn inner(&self) -> FsTaskRepository<'_> {
491 FsTaskRepository::new(&self.ito_path)
492 }
493}
494
495impl TaskRepository for OwnedFsTaskRepository {
496 fn load_tasks(
497 &self,
498 change_id: &str,
499 ) -> ito_domain::errors::DomainResult<ito_domain::tasks::TasksParseResult> {
500 self.inner().load_tasks(change_id)
501 }
502}
503
504#[derive(Debug, Clone)]
505struct OwnedFsSpecRepository {
506 ito_path: PathBuf,
507}
508
509impl OwnedFsSpecRepository {
510 fn new(ito_path: PathBuf) -> Self {
511 Self { ito_path }
512 }
513
514 fn inner(&self) -> FsSpecRepository<'_> {
515 FsSpecRepository::new(&self.ito_path)
516 }
517}
518
519impl SpecRepository for OwnedFsSpecRepository {
520 fn list(&self) -> ito_domain::errors::DomainResult<Vec<ito_domain::specs::SpecSummary>> {
521 self.inner().list()
522 }
523
524 fn get(&self, id: &str) -> ito_domain::errors::DomainResult<ito_domain::specs::SpecDocument> {
525 self.inner().get(id)
526 }
527}