use anyhow::{bail, Context, Result};
use serde::{Deserialize, Serialize};
use snomed_ecl_engine::store::Manifest;
use std::path::{Path, PathBuf};
pub const ENV_STORE: &str = "SNOMED_ECL_STORE";
#[derive(Debug, Default, Deserialize, Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct State {
pub store: Option<PathBuf>,
}
pub fn state_path() -> Result<PathBuf> {
let base = if cfg!(windows) {
std::env::var_os("APPDATA").map(PathBuf::from)
} else {
std::env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.or_else(|| std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".config")))
};
let base = base.context(if cfg!(windows) {
"Cannot locate APPDATA for the selected-index file"
} else {
"Cannot locate XDG_CONFIG_HOME or HOME for the selected-index file"
})?;
Ok(base.join("snomed-ecl-engine").join("state.json"))
}
pub fn load() -> State {
state_path()
.ok()
.and_then(|path| std::fs::read(path).ok())
.and_then(|bytes| serde_json::from_slice(&bytes).ok())
.unwrap_or_default()
}
pub fn save(state: &State) -> Result<()> {
let path = state_path()?;
let directory = path.parent().context("Selected-index path has no parent")?;
std::fs::create_dir_all(directory)
.with_context(|| format!("Cannot create {}", directory.display()))?;
std::fs::write(&path, serde_json::to_vec_pretty(state)?)
.with_context(|| format!("Cannot write {}", path.display()))?;
Ok(())
}
#[derive(Clone, Copy, PartialEq)]
pub enum Source {
Argument,
Environment,
Selected,
}
impl Source {
pub fn describe(self) -> &'static str {
match self {
Self::Argument => "given on the command line",
Self::Environment => "from SNOMED_ECL_STORE",
Self::Selected => "selected earlier with `use`",
}
}
}
pub fn resolve(explicit: Option<&str>) -> Result<(PathBuf, Source)> {
if let Some(path) = explicit {
return Ok((PathBuf::from(path), Source::Argument));
}
if let Some(path) = std::env::var_os(ENV_STORE).filter(|value| !value.is_empty()) {
return Ok((PathBuf::from(path), Source::Environment));
}
if let Some(path) = load().store {
return Ok((path, Source::Selected));
}
bail!(
"No index selected. Give the index path, or select one first:\n\
\x20 snomed-ecl-engine stores list indexes found on disk\n\
\x20 snomed-ecl-engine use PATH remember one index for later commands\n\
Setting {ENV_STORE} overrides the selection for one shell."
)
}
pub struct Found {
pub path: PathBuf,
pub edition: String,
pub active_concepts: usize,
pub concepts: usize,
pub bytes: u64,
pub packed: bool,
pub selected: bool,
}
pub fn default_roots() -> Vec<PathBuf> {
vec![PathBuf::from("."), PathBuf::from("data")]
}
pub fn discover(roots: &[PathBuf], selected: Option<&Path>) -> Vec<Found> {
let mut found = Vec::new();
let mut seen = Vec::new();
for root in roots {
let mut candidates = vec![root.clone()];
if let Ok(entries) = std::fs::read_dir(root) {
candidates.extend(entries.flatten().map(|entry| entry.path()));
}
for candidate in candidates {
let key = std::fs::canonicalize(&candidate).unwrap_or_else(|_| candidate.clone());
if seen.contains(&key) {
continue;
}
seen.push(key.clone());
if let Some(entry) = inspect(&candidate, selected) {
found.push(entry);
}
}
}
found.sort_by(|a, b| a.path.cmp(&b.path));
found
}
pub fn inspect(path: &Path, selected: Option<&Path>) -> Option<Found> {
let packed = path.is_file();
if packed {
let header_matches = std::fs::File::open(path)
.and_then(|mut file| {
use std::io::Read;
let mut magic = [0; 8];
file.read_exact(&mut magic)?;
Ok(&magic == b"SNECL002")
})
.unwrap_or(false);
if !header_matches {
return None;
}
} else if !path.join("manifest.json").is_file() {
return None;
}
let manifest = Manifest::read(path).ok()?;
let canonical = std::fs::canonicalize(path).ok();
let selected = match (selected, &canonical) {
(Some(selected), Some(canonical)) => {
std::fs::canonicalize(selected).is_ok_and(|selected| &selected == canonical)
}
_ => false,
};
Some(Found {
bytes: size_of_index(path, packed),
edition: manifest.edition,
active_concepts: manifest.active_concept_count,
concepts: manifest.concept_count,
path: path.to_path_buf(),
packed,
selected,
})
}
fn size_of_index(path: &Path, packed: bool) -> u64 {
if packed {
return std::fs::metadata(path).map(|m| m.len()).unwrap_or(0);
}
let Ok(entries) = std::fs::read_dir(path) else {
return 0;
};
entries
.flatten()
.map(|entry| match entry.metadata() {
Ok(metadata) if metadata.is_dir() => size_of_index(&entry.path(), false),
Ok(metadata) => metadata.len(),
Err(_) => 0,
})
.sum()
}