use std::path::{Path, PathBuf};
use std::sync::Arc;
use ikigai_core::{EndpointSpace, Iri, Space, SystemClock};
pub const DIR: &str = "layers";
pub const STATE_KEY: &str = "layers.state";
pub const STATE_FLAG: &str = "--layers-state";
pub const SPACE: &str = "urn:ikigai:space:served:layers";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum StateSource {
Flag,
Config,
Default,
}
impl StateSource {
pub fn as_str(self) -> &'static str {
match self {
StateSource::Flag => "flag",
StateSource::Config => "config",
StateSource::Default => "default",
}
}
}
pub struct Hosted {
pub host: Arc<ikigai_layers::Host>,
pub config: PathBuf,
pub state: PathBuf,
pub source: StateSource,
}
impl Hosted {
pub fn layer_lines(&self) -> Vec<String> {
self.host
.layers()
.iter()
.map(|(name, state)| format!("{name} ({})", state.def.model.as_str()))
.collect()
}
}
pub fn configured() -> Option<PathBuf> {
configured_in(&crate::config::config_home()?)
}
pub fn configured_in(home: &Path) -> Option<PathBuf> {
let dir = home.join(DIR);
dir.is_dir().then_some(dir)
}
pub fn state_dir(flag: Option<&Path>) -> Result<(PathBuf, StateSource), String> {
let instance = crate::instance_name();
let configured = crate::config::get(&format!("{instance}.{STATE_KEY}"))
.or_else(|| crate::config::get(STATE_KEY));
state_dir_from(
flag,
configured.as_deref(),
crate::config::config_home().as_deref(),
std::env::var_os("HOME").as_deref().map(Path::new),
instance,
)
}
pub fn state_dir_from(
flag: Option<&Path>,
configured: Option<&str>,
config_home: Option<&Path>,
home: Option<&Path>,
instance: &str,
) -> Result<(PathBuf, StateSource), String> {
if let Some(dir) = flag {
return Ok((dir.to_path_buf(), StateSource::Flag));
}
if let Some(value) = configured.map(str::trim).filter(|v| !v.is_empty()) {
let path = match (value.strip_prefix("~/"), home) {
(Some(rest), Some(home)) => home.join(rest),
_ => PathBuf::from(value),
};
let path = match config_home {
Some(base) if path.is_relative() => base.join(path),
_ => path,
};
return Ok((path, StateSource::Config));
}
let home = home.filter(|h| !h.as_os_str().is_empty()).ok_or_else(|| {
format!(
"layers are configured but no state directory is: pass {STATE_FLAG} <dir>, set \
`{STATE_KEY}` in the config home, or set HOME so the default \
(~/.ikigai/layers/{instance}) has somewhere to be"
)
})?;
Ok((
ikigai_core::config::data_home_in(home)
.join("layers")
.join(instance),
StateSource::Default,
))
}
pub fn open(flag: Option<&Path>) -> Result<Option<Hosted>, String> {
let Some(home) = crate::config::config_home() else {
return match flag {
Some(_) => Err(format!("{STATE_FLAG} needs a config home holding `{DIR}/`")),
None => Ok(None),
};
};
if configured_in(&home).is_none() {
return match flag {
Some(_) => Err(format!(
"{STATE_FLAG} names where layers keep their state, but {} does not exist, so \
no layer is configured",
home.join(DIR).display()
)),
None => Ok(None),
};
}
let (state, source) = state_dir(flag)?;
let host = ikigai_layers::Host::open(&home, &state).map_err(|e| {
format!(
"layers are configured in {} but the host cannot open: {e}",
home.join(DIR).display()
)
})?;
Ok(Some(Hosted {
host,
config: home,
state,
source,
}))
}
pub fn spaces(host: &Arc<ikigai_layers::Host>) -> Vec<Arc<dyn Space>> {
let layers: EndpointSpace = ikigai_layers::space(host)
.named(Iri::parse(SPACE).expect("the layer space's name is an IRI"));
vec![Arc::new(layers) as Arc<dyn Space>]
}
pub fn clock() -> Arc<dyn ikigai_core::Clock> {
Arc::new(SystemClock)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_flag_wins_then_the_config_home_then_the_data_home() {
let flag = Path::new("/srv/flagged");
let home = Path::new("/home/b");
let config = Path::new("/home/b/.config/ikigai");
assert_eq!(
state_dir_from(
Some(flag),
Some("ignored"),
Some(config),
Some(home),
"layers"
),
Ok((flag.to_path_buf(), StateSource::Flag))
);
assert_eq!(
state_dir_from(None, Some("state"), Some(config), Some(home), "layers"),
Ok((config.join("state"), StateSource::Config))
);
assert_eq!(
state_dir_from(
None,
Some("~/layer-state"),
Some(config),
Some(home),
"layers"
),
Ok((home.join("layer-state"), StateSource::Config))
);
assert_eq!(
state_dir_from(
None,
Some("/var/layers"),
Some(config),
Some(home),
"layers"
),
Ok((PathBuf::from("/var/layers"), StateSource::Config))
);
assert_eq!(
state_dir_from(None, None, Some(config), Some(home), "layers"),
Ok((home.join(".ikigai/layers/layers"), StateSource::Default))
);
assert_eq!(
state_dir_from(None, Some(" "), Some(config), Some(home), "edge"),
Ok((home.join(".ikigai/layers/edge"), StateSource::Default)),
"a blank setting is no setting"
);
let err = state_dir_from(None, None, Some(config), None, "layers").unwrap_err();
assert!(err.contains(STATE_FLAG) && err.contains(STATE_KEY), "{err}");
}
#[test]
fn only_a_directory_switches_the_host_on() {
let dir = std::env::temp_dir().join(format!("iki-layers-switch-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
assert_eq!(configured_in(&dir), None);
std::fs::write(dir.join(DIR), "not a directory").unwrap();
assert_eq!(
configured_in(&dir),
None,
"a FILE named layers is not a configuration"
);
std::fs::remove_file(dir.join(DIR)).unwrap();
std::fs::create_dir(dir.join(DIR)).unwrap();
assert_eq!(configured_in(&dir), Some(dir.join(DIR)));
let _ = std::fs::remove_dir_all(&dir);
}
}