use std::path::{Path, PathBuf};
use std::sync::Arc;
use ito_config::ito_dir::{absolutize_and_normalize, lexical_normalize};
use ito_config::types::{ItoConfig, RepositoryPersistenceMode};
use ito_config::{ConfigContext, load_cascading_project_config};
#[cfg(feature = "backend")]
use crate::artifact_mutations::RemoteChangeArtifactBundleClient;
use crate::artifact_mutations::{
BundleBackedChangeArtifactMutationService, FsChangeArtifactMutationService,
SqliteChangeArtifactBundleClient,
};
#[cfg(feature = "backend")]
use crate::backend_change_repository::BackendChangeRepository;
#[cfg(feature = "backend")]
use crate::backend_client::{BackendRuntime, resolve_backend_runtime};
#[cfg(feature = "backend")]
use crate::backend_http::BackendHttpClient;
#[cfg(feature = "backend")]
use crate::backend_module_repository::BackendModuleRepository;
#[cfg(feature = "backend")]
use crate::backend_spec_repository::BackendSpecRepository;
#[cfg(not(feature = "backend"))]
use crate::capabilities::CompiledFeature;
use crate::capabilities::{CapabilityPreflight, preflight_config};
use crate::change_repository::FsChangeRepository;
use crate::errors::{CoreError, CoreResult};
use crate::module_repository::FsModuleRepository;
#[cfg(feature = "backend")]
use crate::remote_task_repository::RemoteTaskRepository;
use crate::spec_repository::FsSpecRepository;
use crate::sqlite_project_store::SqliteBackendProjectStore;
use crate::task_mutations::FsTaskMutationService;
#[cfg(feature = "backend")]
use crate::task_mutations::boxed_fs_task_mutation_service;
use crate::task_repository::FsTaskRepository;
use ito_domain::changes::ChangeArtifactMutationService;
use ito_domain::changes::ChangeRepository;
use ito_domain::modules::ModuleRepository;
use ito_domain::specs::SpecRepository;
use ito_domain::tasks::{TaskMutationService, TaskRepository};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PersistenceMode {
Filesystem,
Sqlite,
Remote,
}
#[derive(Clone)]
pub struct RepositorySet {
pub changes: Arc<dyn ChangeRepository + Send + Sync>,
pub modules: Arc<dyn ModuleRepository + Send + Sync>,
pub tasks: Arc<dyn TaskRepository + Send + Sync>,
pub task_mutations: Arc<dyn TaskMutationService + Send + Sync>,
pub specs: Arc<dyn SpecRepository + Send + Sync>,
}
#[derive(Debug, Clone)]
pub struct SqliteRuntime {
pub db_path: PathBuf,
pub org: String,
pub repo: String,
}
pub struct RepositoryRuntime {
mode: PersistenceMode,
ito_path: PathBuf,
#[cfg(feature = "backend")]
backend_runtime: Option<BackendRuntime>,
sqlite_runtime: Option<SqliteRuntime>,
repositories: RepositorySet,
change_artifact_mutations: Arc<dyn ChangeArtifactMutationService + Send + Sync>,
}
impl RepositoryRuntime {
pub fn mode(&self) -> PersistenceMode {
self.mode
}
pub fn ito_path(&self) -> &Path {
self.ito_path.as_path()
}
#[cfg(feature = "backend")]
pub fn backend_runtime(&self) -> Option<&BackendRuntime> {
self.backend_runtime.as_ref()
}
pub fn sqlite_runtime(&self) -> Option<&SqliteRuntime> {
self.sqlite_runtime.as_ref()
}
pub fn repositories(&self) -> &RepositorySet {
&self.repositories
}
pub fn change_artifact_mutations(&self) -> &(dyn ChangeArtifactMutationService + Send + Sync) {
self.change_artifact_mutations.as_ref()
}
}
#[cfg(feature = "backend")]
pub trait RemoteRepositoryFactory: Send + Sync {
fn build(&self, runtime: &BackendRuntime) -> CoreResult<RepositorySet>;
}
#[cfg(feature = "backend")]
pub struct HttpRemoteRepositoryFactory;
#[cfg(feature = "backend")]
impl RemoteRepositoryFactory for HttpRemoteRepositoryFactory {
fn build(&self, runtime: &BackendRuntime) -> CoreResult<RepositorySet> {
let client = BackendHttpClient::new(runtime.clone());
Ok(RepositorySet {
changes: Arc::new(BackendChangeRepository::new(client.clone())),
modules: Arc::new(BackendModuleRepository::new(client.clone())),
tasks: Arc::new(RemoteTaskRepository::new(client.clone())),
task_mutations: Arc::new(client.clone()),
specs: Arc::new(BackendSpecRepository::new(client.clone())),
})
}
}
pub struct RepositoryRuntimeBuilder {
ito_path: PathBuf,
mode: PersistenceMode,
#[cfg(feature = "backend")]
backend_runtime: Option<BackendRuntime>,
sqlite_runtime: Option<SqliteRuntime>,
#[cfg(feature = "backend")]
remote_factory: Arc<dyn RemoteRepositoryFactory>,
}
impl RepositoryRuntimeBuilder {
pub fn new(ito_path: impl Into<PathBuf>) -> Self {
Self {
ito_path: ito_path.into(),
mode: PersistenceMode::Filesystem,
#[cfg(feature = "backend")]
backend_runtime: None,
sqlite_runtime: None,
#[cfg(feature = "backend")]
remote_factory: Arc::new(HttpRemoteRepositoryFactory),
}
}
pub fn mode(mut self, mode: PersistenceMode) -> Self {
self.mode = mode;
self
}
#[cfg(feature = "backend")]
pub fn backend_runtime(mut self, runtime: BackendRuntime) -> Self {
self.backend_runtime = Some(runtime);
self
}
pub fn sqlite_runtime(mut self, runtime: SqliteRuntime) -> Self {
self.sqlite_runtime = Some(runtime);
self
}
#[cfg(feature = "backend")]
pub fn remote_factory(mut self, factory: Arc<dyn RemoteRepositoryFactory>) -> Self {
self.remote_factory = factory;
self
}
pub fn build(self) -> CoreResult<RepositoryRuntime> {
match self.mode {
PersistenceMode::Filesystem => {
let ito_path = self.ito_path;
let repositories = filesystem_repository_set(&ito_path);
Ok(RepositoryRuntime {
mode: PersistenceMode::Filesystem,
ito_path: ito_path.clone(),
#[cfg(feature = "backend")]
backend_runtime: None,
sqlite_runtime: None,
repositories,
change_artifact_mutations: Arc::new(FsChangeArtifactMutationService::new(
ito_path,
)),
})
}
PersistenceMode::Sqlite => {
let ito_path = self.ito_path;
let runtime = self.sqlite_runtime.ok_or_else(|| {
CoreError::validation("sqlite mode requires sqlite runtime".to_string())
})?;
let repositories = sqlite_repository_set(&runtime)?;
let artifact_mutations = Arc::new(BundleBackedChangeArtifactMutationService::new(
SqliteChangeArtifactBundleClient::new(&runtime),
));
Ok(RepositoryRuntime {
mode: PersistenceMode::Sqlite,
ito_path,
#[cfg(feature = "backend")]
backend_runtime: None,
sqlite_runtime: Some(runtime),
repositories,
change_artifact_mutations: artifact_mutations,
})
}
#[cfg(feature = "backend")]
PersistenceMode::Remote => {
let ito_path = self.ito_path;
let runtime = self.backend_runtime.ok_or_else(|| {
CoreError::validation("remote mode requires backend runtime".to_string())
})?;
let repositories = self.remote_factory.build(&runtime)?;
let artifact_mutations = Arc::new(BundleBackedChangeArtifactMutationService::new(
RemoteChangeArtifactBundleClient::new(BackendHttpClient::new(runtime.clone())),
));
Ok(RepositoryRuntime {
mode: PersistenceMode::Remote,
ito_path,
backend_runtime: Some(runtime),
sqlite_runtime: None,
repositories,
change_artifact_mutations: artifact_mutations,
})
}
#[cfg(not(feature = "backend"))]
PersistenceMode::Remote => Err(CoreError::feature_unavailable(
CompiledFeature::Backend,
"repository.mode=remote",
"use filesystem or sqlite persistence, or install an experimental build with the backend feature",
)),
}
}
}
pub fn resolve_repository_runtime(
ito_path: &Path,
ctx: &ConfigContext,
) -> CoreResult<RepositoryRuntime> {
let project_root = ctx
.project_dir
.as_deref()
.unwrap_or_else(|| ito_path.parent().unwrap_or(ito_path));
let merged = load_cascading_project_config(project_root, ito_path, ctx).merged;
let raw_mode = merged
.pointer("/repository/mode")
.and_then(|v| v.as_str())
.unwrap_or("filesystem");
if RepositoryPersistenceMode::parse_value(raw_mode).is_none() {
let valid = RepositoryPersistenceMode::ALL.join(", ");
return Err(CoreError::validation(format!(
"Invalid repository.mode '{raw_mode}': must be one of {valid}"
)));
}
let config = serde_json::from_value::<ItoConfig>(merged)
.map_err(|err| CoreError::serde("parse resolved Ito config", err.to_string()))?;
preflight_config(&config, CapabilityPreflight::Stateful)?;
if !config.backend.enabled {
return match config.repository.mode {
RepositoryPersistenceMode::Filesystem => {
RepositoryRuntimeBuilder::new(ito_path).build()
}
RepositoryPersistenceMode::Sqlite => {
let runtime = resolve_sqlite_runtime(&config, project_root)?;
RepositoryRuntimeBuilder::new(ito_path)
.mode(PersistenceMode::Sqlite)
.sqlite_runtime(runtime)
.build()
}
};
}
#[cfg(feature = "backend")]
{
let runtime = resolve_backend_runtime(&config.backend)?.ok_or_else(|| {
CoreError::validation(
"Backend mode is enabled but runtime was not resolved".to_string(),
)
})?;
RepositoryRuntimeBuilder::new(ito_path)
.mode(PersistenceMode::Remote)
.backend_runtime(runtime)
.build()
}
#[cfg(not(feature = "backend"))]
Err(CoreError::feature_unavailable(
CompiledFeature::Backend,
"backend.enabled",
"disable backend.enabled, or install an experimental build with the backend feature",
))
}
fn resolve_sqlite_runtime(config: &ItoConfig, project_root: &Path) -> CoreResult<SqliteRuntime> {
let Some(db_path) = config.repository.sqlite.db_path.as_deref() else {
return Err(CoreError::validation(
"SQLite persistence mode requires 'repository.sqlite.dbPath' to be set",
));
};
let db_path = db_path.trim();
if db_path.is_empty() {
return Err(CoreError::validation(
"SQLite persistence mode requires 'repository.sqlite.dbPath' to be set",
));
}
let db_path = PathBuf::from(db_path);
let db_path = if db_path.is_absolute() {
db_path
} else {
project_root.join(db_path)
};
let db_path =
absolutize_and_normalize(&db_path).unwrap_or_else(|_| lexical_normalize(&db_path));
let repo = match project_root.file_name().and_then(|s| s.to_str()) {
Some(name) if !name.trim().is_empty() => name.to_string(),
_ => "project".to_string(),
};
Ok(SqliteRuntime {
db_path,
org: "local".to_string(),
repo,
})
}
fn filesystem_repository_set(ito_path: &Path) -> RepositorySet {
let ito_path = ito_path.to_path_buf();
RepositorySet {
changes: Arc::new(OwnedFsChangeRepository::new(ito_path.clone())),
modules: Arc::new(OwnedFsModuleRepository::new(ito_path.clone())),
tasks: Arc::new(OwnedFsTaskRepository::new(ito_path.clone())),
task_mutations: Arc::new(FsTaskMutationService::new(ito_path.clone())),
specs: Arc::new(OwnedFsSpecRepository::new(ito_path)),
}
}
fn sqlite_repository_set(runtime: &SqliteRuntime) -> CoreResult<RepositorySet> {
let store = SqliteBackendProjectStore::open(&runtime.db_path)?;
store.repository_set(&runtime.org, &runtime.repo)
}
#[cfg(feature = "backend")]
pub(crate) fn boxed_fs_change_repository(ito_path: PathBuf) -> Box<dyn ChangeRepository + Send> {
Box::new(OwnedFsChangeRepository::new(ito_path))
}
#[cfg(feature = "backend")]
pub(crate) fn boxed_fs_module_repository(ito_path: PathBuf) -> Box<dyn ModuleRepository + Send> {
Box::new(OwnedFsModuleRepository::new(ito_path))
}
#[cfg(feature = "backend")]
pub(crate) fn boxed_fs_task_repository(ito_path: PathBuf) -> Box<dyn TaskRepository + Send> {
Box::new(OwnedFsTaskRepository::new(ito_path))
}
#[cfg(feature = "backend")]
pub(crate) fn boxed_fs_task_mutation_port(
ito_path: PathBuf,
) -> Box<dyn TaskMutationService + Send> {
boxed_fs_task_mutation_service(ito_path)
}
#[cfg(feature = "backend")]
pub(crate) fn boxed_fs_spec_repository(ito_path: PathBuf) -> Box<dyn SpecRepository + Send> {
Box::new(OwnedFsSpecRepository::new(ito_path))
}
#[derive(Debug, Clone)]
struct OwnedFsChangeRepository {
ito_path: PathBuf,
}
impl OwnedFsChangeRepository {
fn new(ito_path: PathBuf) -> Self {
Self { ito_path }
}
fn inner(&self) -> FsChangeRepository<'_> {
FsChangeRepository::new(&self.ito_path)
}
}
impl ChangeRepository for OwnedFsChangeRepository {
fn resolve_target_with_options(
&self,
input: &str,
options: ito_domain::changes::ResolveTargetOptions,
) -> ito_domain::changes::ChangeTargetResolution {
self.inner().resolve_target_with_options(input, options)
}
fn suggest_targets(&self, input: &str, max: usize) -> Vec<String> {
self.inner().suggest_targets(input, max)
}
fn exists(&self, id: &str) -> bool {
self.inner().exists(id)
}
fn exists_with_filter(
&self,
id: &str,
filter: ito_domain::changes::ChangeLifecycleFilter,
) -> bool {
self.inner().exists_with_filter(id, filter)
}
fn get_with_filter(
&self,
id: &str,
filter: ito_domain::changes::ChangeLifecycleFilter,
) -> ito_domain::errors::DomainResult<ito_domain::changes::Change> {
self.inner().get_with_filter(id, filter)
}
fn list_with_filter(
&self,
filter: ito_domain::changes::ChangeLifecycleFilter,
) -> ito_domain::errors::DomainResult<Vec<ito_domain::changes::ChangeSummary>> {
self.inner().list_with_filter(filter)
}
fn list_by_module_with_filter(
&self,
module_id: &str,
filter: ito_domain::changes::ChangeLifecycleFilter,
) -> ito_domain::errors::DomainResult<Vec<ito_domain::changes::ChangeSummary>> {
self.inner().list_by_module_with_filter(module_id, filter)
}
fn list_incomplete_with_filter(
&self,
filter: ito_domain::changes::ChangeLifecycleFilter,
) -> ito_domain::errors::DomainResult<Vec<ito_domain::changes::ChangeSummary>> {
self.inner().list_incomplete_with_filter(filter)
}
fn list_complete_with_filter(
&self,
filter: ito_domain::changes::ChangeLifecycleFilter,
) -> ito_domain::errors::DomainResult<Vec<ito_domain::changes::ChangeSummary>> {
self.inner().list_complete_with_filter(filter)
}
fn get_summary_with_filter(
&self,
id: &str,
filter: ito_domain::changes::ChangeLifecycleFilter,
) -> ito_domain::errors::DomainResult<ito_domain::changes::ChangeSummary> {
self.inner().get_summary_with_filter(id, filter)
}
}
#[derive(Debug, Clone)]
struct OwnedFsModuleRepository {
ito_path: PathBuf,
}
impl OwnedFsModuleRepository {
fn new(ito_path: PathBuf) -> Self {
Self { ito_path }
}
fn inner(&self) -> FsModuleRepository<'_> {
FsModuleRepository::new(&self.ito_path)
}
}
impl ModuleRepository for OwnedFsModuleRepository {
fn exists(&self, id: &str) -> bool {
self.inner().exists(id)
}
fn get(
&self,
id_or_name: &str,
) -> ito_domain::errors::DomainResult<ito_domain::modules::Module> {
self.inner().get(id_or_name)
}
fn list(&self) -> ito_domain::errors::DomainResult<Vec<ito_domain::modules::ModuleSummary>> {
self.inner().list()
}
fn list_sub_modules(
&self,
parent_id: &str,
) -> ito_domain::errors::DomainResult<Vec<ito_domain::modules::SubModuleSummary>> {
self.inner().list_sub_modules(parent_id)
}
fn get_sub_module(
&self,
composite_id: &str,
) -> ito_domain::errors::DomainResult<ito_domain::modules::SubModule> {
self.inner().get_sub_module(composite_id)
}
}
#[derive(Debug, Clone)]
struct OwnedFsTaskRepository {
ito_path: PathBuf,
}
impl OwnedFsTaskRepository {
fn new(ito_path: PathBuf) -> Self {
Self { ito_path }
}
fn inner(&self) -> FsTaskRepository<'_> {
FsTaskRepository::new(&self.ito_path)
}
}
impl TaskRepository for OwnedFsTaskRepository {
fn load_tasks(
&self,
change_id: &str,
) -> ito_domain::errors::DomainResult<ito_domain::tasks::TasksParseResult> {
self.inner().load_tasks(change_id)
}
}
#[derive(Debug, Clone)]
struct OwnedFsSpecRepository {
ito_path: PathBuf,
}
impl OwnedFsSpecRepository {
fn new(ito_path: PathBuf) -> Self {
Self { ito_path }
}
fn inner(&self) -> FsSpecRepository<'_> {
FsSpecRepository::new(&self.ito_path)
}
}
impl SpecRepository for OwnedFsSpecRepository {
fn list(&self) -> ito_domain::errors::DomainResult<Vec<ito_domain::specs::SpecSummary>> {
self.inner().list()
}
fn get(&self, id: &str) -> ito_domain::errors::DomainResult<ito_domain::specs::SpecDocument> {
self.inner().get(id)
}
}