use crate::error::AppResult;
use thunderstore_engine::client::ThunderstoreClient;
pub async fn run(client: &ThunderstoreClient, term: &str) -> AppResult<()> {
let manifest = client.get_manifest().await?;
let results = manifest.search(term);
if results.is_empty() {
println!("No mods found matching '{term}'");
return Ok(());
}
println!("Found {} mods matching '{term}':\n", results.len());
for idx in results {
let Some(package) = manifest.get_package_at(idx) else {
continue;
};
let latest = package.latest_version();
let version = latest
.and_then(|v| v.version_number.clone())
.unwrap_or_else(|| "Unknown".to_string());
let description = latest
.and_then(|v| v.description.clone())
.unwrap_or_default();
let identifier = package_identifier(
package.full_name.as_deref(),
package.owner.as_deref(),
package.name.as_deref(),
);
println!("{identifier} ({version})");
if !description.is_empty() {
println!(" {description}");
}
println!();
}
Ok(())
}
fn package_identifier(full_name: Option<&str>, owner: Option<&str>, name: Option<&str>) -> String {
match full_name {
Some(full_name) => full_name.to_string(),
None => format!(
"{}-{}",
owner.unwrap_or("Unknown"),
name.unwrap_or("Unknown")
),
}
}
#[cfg(test)]
mod tests {
use super::*;
use mockito::Server;
use tempfile::tempdir;
use tokio::runtime::Runtime;
fn test_manifest_json() -> &'static str {
r#"[{
"name": "ModA",
"full_name": "Owner-ModA",
"owner": "Owner",
"package_url": "https://example.com/mods/ModA",
"date_created": "2024-01-01T12:00:00Z",
"date_updated": "2024-01-02T12:00:00Z",
"uuid4": "test-uuid",
"rating_score": 5,
"is_pinned": false,
"is_deprecated": false,
"has_nsfw_content": false,
"categories": ["category1"],
"versions": [{
"name": "ModA",
"full_name": "Owner-ModA",
"description": "Test description",
"icon": "icon.png",
"version_number": "1.0.0",
"dependencies": [],
"download_url": "https://example.com/mods/ModA/download",
"downloads": 100,
"date_created": "2024-01-01T12:00:00Z",
"website_url": "https://example.com",
"is_active": true,
"uuid4": "test-version-uuid",
"file_size": 1024
}]
}]"#
}
fn test_client(
server: &mut mockito::ServerGuard,
cache_dir: &std::path::Path,
) -> ThunderstoreClient {
let last_modified = "Wed, 21 Feb 2024 15:30:45 GMT";
server
.mock("GET", "/c/valheim/api/v1/package/")
.with_status(200)
.with_header("Content-Type", "application/json")
.with_header("Last-Modified", last_modified)
.with_body(test_manifest_json())
.create();
ThunderstoreClient::builder()
.package_index_url(format!("{}/c/valheim/api/v1/package/", server.url()))
.cache_dir(cache_dir)
.build()
.unwrap()
}
#[test]
fn test_run_search_no_results() {
let mut server = Server::new();
let temp_dir = tempdir().unwrap();
let client = test_client(&mut server, temp_dir.path());
let rt = Runtime::new().unwrap();
let result = rt.block_on(run(&client, "nonexistent_mod_xyz"));
assert!(result.is_ok());
}
#[test]
fn test_run_search_with_results() {
let mut server = Server::new();
let temp_dir = tempdir().unwrap();
let client = test_client(&mut server, temp_dir.path());
let rt = Runtime::new().unwrap();
let result = rt.block_on(run(&client, "moda"));
assert!(result.is_ok());
}
#[test]
fn test_run_search_case_insensitive() {
let mut server = Server::new();
let temp_dir = tempdir().unwrap();
let client = test_client(&mut server, temp_dir.path());
let rt = Runtime::new().unwrap();
let result = rt.block_on(run(&client, "MODA"));
assert!(result.is_ok());
}
#[test]
fn a_full_name_is_the_whole_identifier_not_a_half_to_prefix() {
assert_eq!(
package_identifier(Some("Owner-ModA"), Some("Owner"), None),
"Owner-ModA"
);
assert_eq!(
package_identifier(Some("Owner-ModA"), Some("Owner"), Some("ModA")),
"Owner-ModA"
);
}
#[test]
fn a_missing_full_name_is_composed_from_its_halves() {
assert_eq!(
package_identifier(None, Some("Owner"), Some("ModA")),
"Owner-ModA"
);
assert_eq!(package_identifier(None, None, Some("ModA")), "Unknown-ModA");
assert_eq!(
package_identifier(None, Some("Owner"), None),
"Owner-Unknown"
);
assert_eq!(package_identifier(None, None, None), "Unknown-Unknown");
}
}