use std::path::{Path, PathBuf};
use crate::runtime_bootstrap::{
DualRootResolution, RealmConfig, RealmLocator, RealmRootChoice, RealmRootDefault,
RuntimeBootstrapError, default_state_root,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StorageLayout {
invocation_context: PathBuf,
project_root: Option<PathBuf>,
user_home_root: Option<PathBuf>,
user_rkat_root: Option<PathBuf>,
credentials_root: Option<PathBuf>,
comms_identity_root: Option<PathBuf>,
state_root: PathBuf,
realm_root_candidates: Vec<PathBuf>,
cache_root: Option<PathBuf>,
}
#[derive(Debug, Clone, Default)]
pub struct StorageLayoutInputs {
pub invocation_context: PathBuf,
pub explicit_state_root: Option<PathBuf>,
pub user_config_root: Option<PathBuf>,
pub default_root: Option<RealmRootDefault>,
pub probe_local_candidate: bool,
}
#[derive(Debug, Clone)]
pub struct ResolvedStorage {
pub layout: StorageLayout,
pub locator: RealmLocator,
pub root_choice: RealmRootChoice,
}
impl StorageLayout {
pub fn resolve(
inputs: StorageLayoutInputs,
realm: &RealmConfig,
) -> Result<ResolvedStorage, RuntimeBootstrapError> {
Self::resolve_with_global_candidate(inputs, realm, &default_state_root())
}
pub fn resolve_with_global_candidate(
inputs: StorageLayoutInputs,
realm: &RealmConfig,
global_candidate: &Path,
) -> Result<ResolvedStorage, RuntimeBootstrapError> {
let project_root = find_project_root(&inputs.invocation_context);
let local_candidate = inputs.probe_local_candidate.then(|| {
local_realms_candidate(
project_root
.as_deref()
.unwrap_or(&inputs.invocation_context),
)
});
let mut realm_config = realm.clone();
if realm_config.state_root.is_none() {
realm_config.state_root = inputs.explicit_state_root.clone();
}
let default_root = inputs.default_root.unwrap_or(RealmRootDefault::UserGlobal);
let DualRootResolution {
locator,
choice,
candidate_roots,
} = realm_config.resolve_locator_dual_root_with(
local_candidate.as_deref(),
global_candidate,
default_root,
)?;
let user_home_root = inputs.user_config_root.clone().or_else(dirs::home_dir);
let user_rkat_root = user_home_root.as_ref().map(|home| home.join(".rkat"));
let layout = StorageLayout {
invocation_context: inputs.invocation_context,
project_root,
user_home_root,
user_rkat_root,
credentials_root: None,
comms_identity_root: None,
state_root: locator.state_root.clone(),
realm_root_candidates: candidate_roots,
cache_root: None,
};
Ok(ResolvedStorage {
layout,
locator,
root_choice: choice,
})
}
pub fn with_injected_roots(
invocation_context: PathBuf,
project_root: Option<PathBuf>,
user_home_root: Option<PathBuf>,
state_root: PathBuf,
) -> Self {
let user_rkat_root = user_home_root.as_ref().map(|home| home.join(".rkat"));
Self {
invocation_context,
project_root,
user_home_root,
user_rkat_root,
credentials_root: None,
comms_identity_root: None,
realm_root_candidates: vec![state_root.clone()],
state_root,
cache_root: None,
}
}
pub fn invocation_context(&self) -> &Path {
&self.invocation_context
}
pub fn project_root(&self) -> Option<&Path> {
self.project_root.as_deref()
}
pub fn user_home_root(&self) -> Option<&Path> {
self.user_home_root.as_deref()
}
pub fn user_rkat_root(&self) -> Option<&Path> {
self.user_rkat_root.as_deref()
}
pub fn credentials_root(&self) -> Option<&Path> {
self.credentials_root.as_deref()
}
pub fn comms_identity_root(&self) -> Option<&Path> {
self.comms_identity_root.as_deref()
}
pub fn state_root(&self) -> &Path {
&self.state_root
}
pub fn realm_root_candidates(&self) -> &[PathBuf] {
&self.realm_root_candidates
}
pub fn cache_root(&self) -> Option<&Path> {
self.cache_root.as_deref()
}
pub fn global_config_path(&self) -> Option<PathBuf> {
self.user_rkat_root.as_ref().map(|r| r.join("config.toml"))
}
#[must_use]
pub fn with_credentials_root(mut self, root: PathBuf) -> Self {
self.credentials_root = Some(root);
self
}
#[must_use]
pub fn with_comms_identity_root(mut self, root: PathBuf) -> Self {
self.comms_identity_root = Some(root);
self
}
#[must_use]
pub fn with_cache_root(mut self, root: PathBuf) -> Self {
self.cache_root = Some(root);
self
}
}
pub fn local_realms_candidate(context_root: &Path) -> PathBuf {
context_root.join(".rkat").join("realms")
}
pub fn find_project_root(start_dir: &Path) -> Option<PathBuf> {
let mut current = start_dir.to_path_buf();
loop {
if current.join(".rkat").exists() {
return Some(current);
}
if !current.pop() {
return None;
}
}
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
use crate::runtime_bootstrap::RealmSelection;
#[test]
fn find_project_root_walks_up_and_accepts_any_rkat_entry() {
let tmp = tempfile::tempdir().expect("tempdir");
let project = tmp.path().join("proj");
let nested = project.join("a/b");
std::fs::create_dir_all(&nested).expect("mkdir");
assert_eq!(find_project_root(&nested), None);
std::fs::create_dir_all(project.join(".rkat")).expect("mkdir .rkat");
assert_eq!(
find_project_root(&nested).as_deref(),
Some(project.as_path())
);
let file_project = tmp.path().join("file-proj");
std::fs::create_dir_all(&file_project).expect("mkdir");
std::fs::write(file_project.join(".rkat"), b"marker").expect("write");
assert_eq!(find_project_root(&file_project), Some(file_project));
}
#[test]
fn resolve_derives_user_rkat_root_from_override() {
let tmp = tempfile::tempdir().expect("tempdir");
let home = tmp.path().join("home");
let inputs = StorageLayoutInputs {
invocation_context: tmp.path().to_path_buf(),
explicit_state_root: Some(tmp.path().join("state")),
user_config_root: Some(home.clone()),
default_root: Some(RealmRootDefault::ProjectLocal),
probe_local_candidate: true,
};
let realm = RealmConfig {
selection: RealmSelection::Explicit {
realm_id: "team".into(),
},
..RealmConfig::default()
};
let resolved = StorageLayout::resolve_with_global_candidate(
inputs,
&realm,
&tmp.path().join("global"),
)
.expect("resolve");
assert_eq!(resolved.root_choice, RealmRootChoice::Explicit);
assert_eq!(resolved.layout.user_home_root(), Some(home.as_path()));
assert_eq!(
resolved.layout.user_rkat_root(),
Some(home.join(".rkat").as_path())
);
assert_eq!(
resolved.layout.global_config_path(),
Some(home.join(".rkat").join("config.toml"))
);
assert!(
!resolved
.layout
.global_config_path()
.expect("path")
.to_string_lossy()
.contains(".rkat/.rkat")
);
}
#[test]
fn cli_shaped_resolution_defaults_to_local_candidate() {
let tmp = tempfile::tempdir().expect("tempdir");
let context = tmp.path().join("ws");
std::fs::create_dir_all(&context).expect("mkdir");
let inputs = StorageLayoutInputs {
invocation_context: context.clone(),
explicit_state_root: None,
user_config_root: None,
default_root: Some(RealmRootDefault::ProjectLocal),
probe_local_candidate: true,
};
let realm = RealmConfig {
selection: RealmSelection::WorkspaceDerived {
root: context.clone(),
},
..RealmConfig::default()
};
let resolved = StorageLayout::resolve_with_global_candidate(
inputs,
&realm,
&tmp.path().join("global"),
)
.expect("resolve");
assert_eq!(resolved.root_choice, RealmRootChoice::Default);
assert_eq!(
resolved.layout.state_root(),
local_realms_candidate(&context).as_path()
);
assert!(resolved.locator.realm.as_str().starts_with("ws-"));
}
#[test]
fn nested_invocation_context_resolves_project_root_local_candidate() {
let tmp = tempfile::tempdir().expect("tempdir");
let project = tmp.path().join("proj");
let nested = project.join("a").join("b");
std::fs::create_dir_all(&nested).expect("mkdir");
std::fs::create_dir_all(project.join(".rkat")).expect("mkdir .rkat");
let inputs = StorageLayoutInputs {
invocation_context: nested.clone(),
explicit_state_root: None,
user_config_root: None,
default_root: Some(RealmRootDefault::ProjectLocal),
probe_local_candidate: true,
};
let realm = RealmConfig {
selection: RealmSelection::Explicit {
realm_id: "team".into(),
},
..RealmConfig::default()
};
let resolved = StorageLayout::resolve_with_global_candidate(
inputs,
&realm,
&tmp.path().join("global"),
)
.expect("resolve");
assert_eq!(resolved.layout.project_root(), Some(project.as_path()));
assert_eq!(resolved.layout.invocation_context(), nested.as_path());
assert_eq!(
resolved.layout.state_root(),
local_realms_candidate(&project).as_path()
);
assert_eq!(
resolved.layout.realm_root_candidates(),
&[local_realms_candidate(&project), tmp.path().join("global")]
);
}
#[test]
fn injected_roots_carry_a_single_candidate() {
let layout = StorageLayout::with_injected_roots(
PathBuf::from("/ctx"),
None,
None,
PathBuf::from("/state"),
);
assert_eq!(
layout.realm_root_candidates(),
&[PathBuf::from("/state")],
"injected layouts resolve single-root: no cross-candidate probing happened"
);
}
#[test]
fn server_shaped_resolution_without_context_never_probes_local() {
let tmp = tempfile::tempdir().expect("tempdir");
let cwd = tmp.path().join("cwd");
let local = local_realms_candidate(&cwd);
std::fs::create_dir_all(local.join("team")).expect("mkdir");
std::fs::write(
local
.join("team")
.join(crate::runtime_bootstrap::REALM_MANIFEST_FILE_NAME),
b"{}",
)
.expect("manifest");
let inputs = StorageLayoutInputs {
invocation_context: cwd,
explicit_state_root: None,
user_config_root: None,
default_root: Some(RealmRootDefault::UserGlobal),
probe_local_candidate: false,
};
let realm = RealmConfig {
selection: RealmSelection::Explicit {
realm_id: "team".into(),
},
..RealmConfig::default()
};
let global = tmp.path().join("global");
let resolved =
StorageLayout::resolve_with_global_candidate(inputs, &realm, &global).expect("resolve");
assert_eq!(resolved.root_choice, RealmRootChoice::Default);
assert_eq!(resolved.layout.state_root(), global.as_path());
}
}