use super::access_marks::mark_access;
use crate::cli::{DogsArgs, Format};
use crate::commands::rpc::typed_request_error;
use crate::dog_index::{self, AvailableDog, DogSourceKind, Origin};
use crate::exit::ExitCode;
use crate::fetch;
use crate::output::{AvailableDogRows, DogRows, Streams, emit, write_outcome};
use shep_client::Client;
use shep_core::paths::ShepPaths;
pub async fn dogs(
client: &Client,
streams: &mut Streams<'_>,
paths: &ShepPaths,
args: &DogsArgs,
) -> ExitCode {
let filter = args.filter.as_deref();
match client.list_flock().await {
Ok(mut procs) => {
mark_access(&mut procs, paths);
procs.retain(|p| p.dog.is_some());
procs.retain(|p| filter.is_none_or(|f| matches_filter(f, &[&p.name])));
let origins = super::flock_display::local_origins(paths, &procs);
let rows = DogRows::with_origins(procs, origins);
write_outcome(emit(
&mut *streams.out,
streams.fmt,
"dogs",
rows,
streams.style,
))
}
Err(err) => typed_request_error(streams, &err),
}
}
fn matches_filter(filter: &str, haystacks: &[&str]) -> bool {
let filter = filter.to_lowercase();
haystacks.iter().any(|h| h.to_lowercase().contains(&filter))
}
pub async fn available_dogs(
streams: &mut Streams<'_>,
paths: &ShepPaths,
args: &DogsArgs,
) -> ExitCode {
let url = dog_index::index_url();
let loaded = match dog_index::cached_index(paths, &url, args.refresh).await {
Ok(loaded) => loaded,
Err(err) => {
let message = format!(
"reading the dog index from {}: {err}",
fetch::url_for_message(&url)
);
return streams.fail(ExitCode::Failure, &message);
}
};
let origin = loaded.origin;
let index = loaded.index;
let (skipped, sanitised) = (index.skipped, index.sanitised);
let filter = args.filter.as_deref();
let matched: Vec<AvailableDog> = index
.dogs
.into_iter()
.filter(|dog| {
filter.is_none_or(|f| matches_filter(f, &[&dog.name, &dog.package, &dog.description]))
})
.collect();
let code = if streams.fmt == Format::Table
&& matched.is_empty()
&& let Some(filter) = filter
{
let _ = writeln!(streams.out, "no dog matches {filter:?}");
ExitCode::Success
} else if streams.fmt == Format::Table
&& let [only] = matched.as_slice()
{
write_outcome(render_detail(&mut *streams.out, only))
} else {
write_outcome(emit(
&mut *streams.out,
streams.fmt,
"dogs",
AvailableDogRows(matched),
streams.style,
))
};
note_index_costs(streams, skipped, sanitised);
note_stale_copy(streams, &url, &origin);
code
}
fn note_stale_copy(streams: &mut Streams<'_>, url: &str, origin: &Origin) {
if let Origin::Stale { age, cause } = origin {
streams.aside(
"dogs_index_stale",
&format!(
"could not read the dog index from {}: {cause}; listing the copy cached {} ago",
fetch::url_for_message(url),
dog_index::describe_age(*age)
),
);
}
}
const INDEX_WIDE: &str = ", across the whole index rather than this listing";
fn note_index_costs(streams: &mut Streams<'_>, skipped: usize, sanitised: usize) {
if skipped > 0 {
streams.aside(
"dogs_skipped",
&format!(
"{skipped} entr{} skipped{INDEX_WIDE}",
if skipped == 1 { "y" } else { "ies" }
),
);
}
if sanitised > 0 {
streams.aside(
"dogs_sanitised",
&format!(
"{sanitised} entr{} contained control characters{INDEX_WIDE}",
if sanitised == 1 { "y" } else { "ies" }
),
);
}
}
pub(crate) fn render_detail(
out: &mut dyn std::io::Write,
dog: &AvailableDog,
) -> std::io::Result<()> {
writeln!(out, "{} . {} . {}", dog.name, dog.package, dog.category)?;
writeln!(out, "{}", dog.description)?;
writeln!(out, "{} . {}", dog.license, dog.repo)?;
writeln!(out)?;
writeln!(out, "{}", install_line(&dog.source, &dog.package))?;
writeln!(
out,
"{}",
adopt_line(&dog.source, &dog.adopt_as, &dog.package)
)
}
pub(crate) fn install_instruction(dog: &AvailableDog) -> String {
install_line(&dog.source, &dog.package).trim().to_owned()
}
fn install_line(source: &DogSourceKind, package: &str) -> String {
match source {
DogSourceKind::Cargo {
version: Some(version),
..
} => {
format!(" $ cargo install {package} --version {version}")
}
DogSourceKind::Cargo { version: None, .. } => {
format!(" not installable: the index approves no version of {package}")
}
DogSourceKind::CargoGit { url } => format!(" $ cargo install --git {url}"),
DogSourceKind::GoInstall { module } => format!(" $ go install {module}@latest"),
DogSourceKind::Manual { instructions } => format!(" {instructions}"),
}
}
fn adopt_line(source: &DogSourceKind, adopt_as: &str, package: &str) -> String {
match source {
DogSourceKind::Cargo { .. } | DogSourceKind::CargoGit { .. } => {
format!(" $ shep adopt ~/.cargo/bin/{package} --name {adopt_as}")
}
DogSourceKind::GoInstall { .. } => {
format!(" $ shep adopt $(go env GOPATH)/bin/{package} --name {adopt_as}")
}
DogSourceKind::Manual { .. } => {
format!(" $ shep adopt <path to the binary> --name {adopt_as}")
}
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use std::time::Duration;
use super::*;
use crate::dog_index::IndexError;
use crate::fetch::FetchError;
fn cargo(version: Option<&str>) -> DogSourceKind {
DogSourceKind::Cargo {
version: version.map(str::to_owned),
sha256: BTreeMap::new(),
}
}
#[test]
fn a_versioned_cargo_row_installs_the_exact_version() {
assert_eq!(
install_line(&cargo(Some("0.2.4")), "shep-log-rotate"),
" $ cargo install shep-log-rotate --version 0.2.4"
);
}
#[test]
fn an_unversioned_cargo_row_says_it_is_not_installable_instead_of_a_command() {
let line = install_line(&cargo(None), "shep-log-rotate");
assert!(line.contains("not installable"), "{line}");
assert!(!line.contains("cargo install"), "{line}");
}
#[test]
fn a_stale_copy_is_announced_in_one_line_naming_its_age() {
let (mut out, mut err) = (Vec::new(), Vec::new());
let mut streams = Streams::bare(&mut out, &mut err, Format::Table);
let origin = Origin::Stale {
age: Duration::from_secs(30 * 60 * 60),
cause: IndexError::Fetch(FetchError::Timeout),
};
note_stale_copy(&mut streams, "https://example.test/dogs.json", &origin);
let printed = String::from_utf8(err).unwrap();
assert_eq!(printed.lines().count(), 1, "{printed}");
assert!(printed.contains("30 hours"), "{printed}");
assert!(
printed.contains("https://example.test/dogs.json"),
"{printed}"
);
assert!(out.is_empty());
}
#[test]
fn a_fetched_or_fresh_cached_listing_prints_no_stale_line() {
for origin in [Origin::Fetched, Origin::Cache] {
let (mut out, mut err) = (Vec::new(), Vec::new());
let mut streams = Streams::bare(&mut out, &mut err, Format::Table);
note_stale_copy(&mut streams, "https://example.test/dogs.json", &origin);
assert!(err.is_empty(), "{origin:?}");
}
}
}