use ostraka_adapter::process::ProcessAdapter;
use ostraka_adapter::{Availability, Profile, VendorAdapter};
#[derive(Debug)]
pub struct Found {
pub id: String,
pub availability: Availability,
}
impl Found {
pub fn ready(&self) -> bool {
self.availability.is_ready()
}
}
pub fn describe(availability: &Availability) -> String {
match availability {
Availability::Ready { version } => version.clone().unwrap_or_default(),
Availability::NotFound { command } => format!("{command} not found on PATH"),
Availability::Unusable { reason } => reason.clone(),
}
}
pub fn unconfigured(configured: &[String]) -> Vec<Found> {
let mut found: Vec<Found> = crate::init::TEMPLATES
.iter()
.filter_map(|(name, text)| {
let id = name.strip_suffix(".toml").unwrap_or(name);
if configured.iter().any(|c| c == id) {
return None;
}
let profile = Profile::parse(text).ok()?;
let adapter = ProcessAdapter::new(profile);
Some(Found {
id: adapter.id().to_string(),
availability: adapter.probe(),
})
})
.collect();
found.sort_by(|a, b| a.id.cmp(&b.id));
found
}
pub fn suggestion(found: &[Found]) -> Option<String> {
let ready: Vec<&str> = found
.iter()
.filter(|f| f.ready())
.map(|f| f.id.as_str())
.collect();
if ready.is_empty() {
return None;
}
Some(format!(
"found on PATH but not configured here: {}. `ostraka init` writes a profile for each",
ready.join(", ")
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn what_is_already_configured_is_not_offered_again() {
let all: Vec<String> = crate::init::TEMPLATES
.iter()
.map(|(name, _)| name.strip_suffix(".toml").unwrap_or(name).to_string())
.collect();
assert!(
unconfigured(&all).is_empty(),
"a fully configured workspace was offered profiles it already has"
);
assert_eq!(
unconfigured(&[]).len(),
crate::init::TEMPLATES.len(),
"an empty workspace was not offered everything this binary ships"
);
}
#[test]
fn nothing_is_suggested_when_nothing_answers() {
let absent = vec![Found {
id: "nowhere".into(),
availability: Availability::NotFound {
command: "nowhere".into(),
},
}];
assert!(suggestion(&absent).is_none());
assert!(suggestion(&[]).is_none());
}
#[test]
fn only_what_answers_is_named() {
let mixed = vec![
Found {
id: "here".into(),
availability: Availability::Ready {
version: Some("1.0".into()),
},
},
Found {
id: "gone".into(),
availability: Availability::NotFound {
command: "gone".into(),
},
},
];
let said = suggestion(&mixed).expect("something answered");
assert!(said.contains("here"), "{said}");
assert!(!said.contains("gone"), "{said}");
assert!(said.contains("ostraka init"), "{said}");
}
}
#[derive(Debug)]
pub struct NoAdapter {
pub said: String,
pub found: Vec<Found>,
}
impl std::fmt::Display for NoAdapter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.said)?;
if let Some(said) = suggestion(&self.found) {
write!(f, "\n\n{said}")?;
}
Ok(())
}
}
impl std::error::Error for NoAdapter {}