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