refactor: rename aim to upm and extract appimage module

This commit is contained in:
stoorps 2026-03-21 22:39:11 +00:00
parent af13e98eb3
commit 863c57e473
Signed by: stoorps
SSH key fingerprint: SHA256:AZlPfu9hTu042EGtZElmDQoy+KvMOeShLDan/fYLoNI
117 changed files with 2622 additions and 887 deletions

View file

@ -0,0 +1,14 @@
[package]
name = "upm-appimage"
version.workspace = true
edition.workspace = true
license.workspace = true
[lib]
path = "src/lib.rs"
[dependencies]
quick-xml.workspace = true
reqwest.workspace = true
serde.workspace = true
upm-core = { path = "../upm-core" }

View file

@ -0,0 +1,163 @@
use crate::source::appimagehub::{
AppImageHubError, AppImageHubTransport, resolve_appimagehub_item, resolve_appimagehub_item_with,
};
use upm_core::adapters::traits::{
AdapterCapabilities, AdapterError, AdapterResolution, AdapterResolveOutcome, SourceAdapter,
};
use upm_core::app::providers::{ExternalAddProvider, ExternalAddResolution};
use upm_core::app::query::resolve_query;
use upm_core::domain::source::{ResolvedRelease, SourceKind, SourceRef};
use upm_core::domain::update::{
ArtifactCandidate, ChannelPreference, UpdateChannelKind, UpdateStrategy,
};
pub struct AppImageHubAdapter;
impl AppImageHubAdapter {
pub fn resolve_source_with<T: AppImageHubTransport + ?Sized>(
&self,
source: &SourceRef,
transport: &T,
) -> Result<AdapterResolveOutcome, AdapterError> {
if source.kind != SourceKind::AppImageHub {
return Err(AdapterError::UnsupportedSource);
}
let resolved = resolve_appimagehub_item_with(source, transport)
.map_err(|error| AdapterError::ResolutionFailed(render_appimagehub_error(&error)))?;
match resolved {
Some(item) => Ok(AdapterResolveOutcome::Resolved(AdapterResolution {
source: item.source,
release: ResolvedRelease {
version: item.version,
prerelease: false,
},
})),
None => Ok(AdapterResolveOutcome::NoInstallableArtifact {
source: source.clone(),
}),
}
}
}
impl SourceAdapter for AppImageHubAdapter {
fn id(&self) -> &'static str {
"appimagehub"
}
fn capabilities(&self) -> AdapterCapabilities {
AdapterCapabilities {
supports_search: true,
supports_exact_resolution: true,
}
}
fn repository_source_kind(&self) -> Option<SourceKind> {
Some(SourceKind::AppImageHub)
}
fn normalize(&self, query: &str) -> Result<SourceRef, AdapterError> {
let source = resolve_query(query).map_err(|_| AdapterError::UnsupportedQuery)?;
if source.kind != SourceKind::AppImageHub {
return Err(AdapterError::UnsupportedQuery);
}
Ok(source)
}
fn resolve(&self, source: &SourceRef) -> Result<AdapterResolution, AdapterError> {
match resolve_appimagehub_item(source)
.map_err(|error| AdapterError::ResolutionFailed(render_appimagehub_error(&error)))?
{
Some(item) => Ok(AdapterResolution {
source: item.source,
release: ResolvedRelease {
version: item.version,
prerelease: false,
},
}),
None => Err(AdapterError::ResolutionFailed(
"appimagehub item has no installable AppImage artifact".to_owned(),
)),
}
}
fn resolve_supported_source(
&self,
source: &SourceRef,
) -> Result<AdapterResolveOutcome, AdapterError> {
let transport = crate::source::appimagehub::default_transport();
self.resolve_source_with(source, transport.as_ref())
}
}
pub struct AppImageHubAddProvider<'a, T: AppImageHubTransport + ?Sized> {
transport: &'a T,
}
impl<'a, T: AppImageHubTransport + ?Sized> AppImageHubAddProvider<'a, T> {
pub fn new(transport: &'a T) -> Self {
Self { transport }
}
}
impl<T: AppImageHubTransport + ?Sized> ExternalAddProvider for AppImageHubAddProvider<'_, T> {
fn id(&self) -> &'static str {
"appimagehub"
}
fn resolve(&self, source: &SourceRef) -> Result<Option<ExternalAddResolution>, AdapterError> {
if source.kind != SourceKind::AppImageHub {
return Ok(None);
}
let adapter = AppImageHubAdapter;
let resolution = match adapter.resolve_source_with(source, self.transport)? {
AdapterResolveOutcome::Resolved(resolution) => resolution,
AdapterResolveOutcome::NoInstallableArtifact { .. } => return Ok(None),
};
let Some(resolved_item) = resolve_appimagehub_item_with(source, self.transport)
.map_err(|error| AdapterError::ResolutionFailed(format!("{error:?}")))?
else {
return Ok(None);
};
Ok(Some(ExternalAddResolution {
resolution,
selected_artifact: ArtifactCandidate {
url: resolved_item.download.url.clone(),
version: resolved_item.version.clone(),
arch: resolved_item.download.arch.clone(),
trusted_checksum: None,
weak_checksum_md5: resolved_item.download.md5sum.clone(),
selection_reason: "provider-release".to_owned(),
},
update_strategy: UpdateStrategy {
preferred: ChannelPreference {
kind: UpdateChannelKind::DirectAsset,
locator: resolved_item.download.url.clone(),
reason: "provider-release".to_owned(),
},
alternates: Vec::new(),
},
display_name_hint: Some(resolved_item.title),
}))
}
}
fn render_appimagehub_error(error: &AppImageHubError) -> String {
match error {
AppImageHubError::FixtureItemMissing(id) => {
format!("missing appimagehub fixture item {id}")
}
AppImageHubError::InsecureDownloadUrl(url) => {
format!("insecure appimagehub download url: {url}")
}
AppImageHubError::Parse(error) => error.to_string(),
AppImageHubError::Transport(error) => error.to_string(),
AppImageHubError::UnsupportedSource(locator) => {
format!("unsupported appimagehub source: {locator}")
}
}
}

View file

@ -0,0 +1,6 @@
pub mod add;
pub mod search;
pub mod source;
pub use add::{AppImageHubAdapter, AppImageHubAddProvider};
pub use search::AppImageHubSearchProvider;

View file

@ -0,0 +1,103 @@
use crate::source::appimagehub::{
AppImageHubSearchError, AppImageHubTransport, search_appimagehub_with,
};
use upm_core::app::search::{SearchProvider, SearchProviderError};
use upm_core::domain::search::{SearchInstallStatus, SearchQuery, SearchResult};
pub struct AppImageHubSearchProvider<'a, T: AppImageHubTransport + ?Sized> {
transport: &'a T,
}
impl<'a, T: AppImageHubTransport + ?Sized> AppImageHubSearchProvider<'a, T> {
pub fn new(transport: &'a T) -> Self {
Self { transport }
}
}
impl<T: AppImageHubTransport + ?Sized> SearchProvider for AppImageHubSearchProvider<'_, T> {
fn search(&self, query: &SearchQuery) -> Result<Vec<SearchResult>, SearchProviderError> {
let hits = search_appimagehub_with(&query.text, query.remote_limit, self.transport)
.map_err(|error| {
SearchProviderError::new("appimagehub", &render_appimagehub_search_error(&error))
})?;
let normalized_query = normalize_lookup(&query.text);
let mut ranked_hits = hits
.into_iter()
.enumerate()
.map(|(index, hit)| {
(
appimagehub_remote_match_rank(
&normalized_query,
&hit.name,
hit.summary.as_deref(),
),
index,
hit,
)
})
.collect::<Vec<_>>();
ranked_hits.sort_by(|left, right| left.0.cmp(&right.0).then(left.1.cmp(&right.1)));
Ok(ranked_hits
.into_iter()
.map(|(_, _, hit)| SearchResult {
provider_id: "appimagehub".to_owned(),
display_name: hit.name,
description: hit.summary,
source_locator: hit.detail_page,
install_query: format!("appimagehub/{}", hit.id),
canonical_locator: hit.id,
version: Some(hit.version),
install_status: SearchInstallStatus::Available,
})
.collect())
}
}
fn normalize_lookup(value: &str) -> String {
value.trim().to_ascii_lowercase()
}
fn appimagehub_remote_match_rank(query: &str, name: &str, summary: Option<&str>) -> u8 {
let name = normalize_lookup(name);
let summary = summary.map(normalize_lookup);
if name == query {
return 0;
}
if name.starts_with(query) {
return 1;
}
if name.contains(query) {
return 2;
}
if summary
.as_deref()
.map(|summary| summary.starts_with(query))
.unwrap_or(false)
{
return 3;
}
if summary
.as_deref()
.map(|summary| summary.contains(query))
.unwrap_or(false)
{
return 4;
}
5
}
fn render_appimagehub_search_error(error: &AppImageHubSearchError) -> String {
match error {
AppImageHubSearchError::Parse(inner) => inner.to_string(),
AppImageHubSearchError::Transport(inner) => inner.to_string(),
}
}

View file

@ -0,0 +1,517 @@
use std::env;
use std::time::Duration;
use upm_core::domain::source::SourceRef;
const DEFAULT_APPIMAGEHUB_API_BASE: &str = "https://api.appimagehub.com/ocs/v1/content";
const GLOBAL_FIXTURE_MODE_ENV: &str = "UPM_FIXTURE_MODE";
const FIXTURE_MODE_ENV: &str = "UPM_APPIMAGEHUB_FIXTURE_MODE";
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AppImageHubDownload {
pub url: String,
pub name: String,
pub package_type: Option<String>,
pub arch: Option<String>,
pub md5sum: Option<String>,
pub version: Option<String>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AppImageHubItem {
pub id: String,
pub name: String,
pub version: String,
pub summary: Option<String>,
pub detail_page: String,
pub tags: Vec<String>,
pub downloads: Vec<AppImageHubDownload>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AppImageHubSearchHit {
pub id: String,
pub name: String,
pub version: String,
pub summary: Option<String>,
pub detail_page: String,
pub tags: Vec<String>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ResolvedAppImageHubItem {
pub source: SourceRef,
pub title: String,
pub version: String,
pub download: AppImageHubDownload,
}
pub trait AppImageHubTransport {
fn fetch_item(&self, id: &str) -> Result<AppImageHubItem, AppImageHubError>;
fn search_items(
&self,
query: &str,
limit: usize,
) -> Result<Vec<AppImageHubSearchHit>, AppImageHubSearchError>;
}
pub fn default_transport() -> Box<dyn AppImageHubTransport> {
if env::var(GLOBAL_FIXTURE_MODE_ENV).ok().as_deref() == Some("1")
|| env::var(FIXTURE_MODE_ENV).ok().as_deref() == Some("1")
{
Box::new(FixtureAppImageHubTransport)
} else {
Box::new(ReqwestAppImageHubTransport::new())
}
}
pub fn resolve_appimagehub_item(
source: &SourceRef,
) -> Result<Option<ResolvedAppImageHubItem>, AppImageHubError> {
let transport = default_transport();
resolve_appimagehub_item_with(source, transport.as_ref())
}
pub fn resolve_appimagehub_item_with<T: AppImageHubTransport + ?Sized>(
source: &SourceRef,
transport: &T,
) -> Result<Option<ResolvedAppImageHubItem>, AppImageHubError> {
let item = transport.fetch_item(source_id(source)?)?;
let Some(download) = item
.downloads
.iter()
.find(|download| is_appimage_download(download))
else {
return Ok(None);
};
validate_download_url(&download.url)?;
Ok(Some(ResolvedAppImageHubItem {
source: source.clone(),
title: item.name.clone(),
version: resolved_version(&item, download),
download: download.clone(),
}))
}
pub fn search_appimagehub(
query: &str,
limit: usize,
) -> Result<Vec<AppImageHubSearchHit>, AppImageHubSearchError> {
let transport = default_transport();
search_appimagehub_with(query, limit, transport.as_ref())
}
pub fn search_appimagehub_with<T: AppImageHubTransport + ?Sized>(
query: &str,
limit: usize,
transport: &T,
) -> Result<Vec<AppImageHubSearchHit>, AppImageHubSearchError> {
transport.search_items(query, limit)
}
pub struct ReqwestAppImageHubTransport {
client: reqwest::blocking::Client,
api_base: String,
}
impl Default for ReqwestAppImageHubTransport {
fn default() -> Self {
Self::new()
}
}
impl ReqwestAppImageHubTransport {
pub fn new() -> Self {
Self {
client: reqwest::blocking::Client::builder()
.timeout(Duration::from_secs(30))
.build()
.expect("reqwest client should build"),
api_base: env::var("UPM_APPIMAGEHUB_API_BASE")
.unwrap_or_else(|_| DEFAULT_APPIMAGEHUB_API_BASE.to_owned()),
}
}
}
impl AppImageHubTransport for ReqwestAppImageHubTransport {
fn fetch_item(&self, id: &str) -> Result<AppImageHubItem, AppImageHubError> {
let url = format!("{}/data/{id}", self.api_base);
let xml = self
.client
.get(url)
.send()
.map_err(AppImageHubError::Transport)?
.error_for_status()
.map_err(AppImageHubError::Transport)?
.text()
.map_err(AppImageHubError::Transport)?;
parse_item_xml(&xml)
}
fn search_items(
&self,
query: &str,
limit: usize,
) -> Result<Vec<AppImageHubSearchHit>, AppImageHubSearchError> {
let url = format!("{}/data", self.api_base);
let xml = self
.client
.get(url)
.query(&[("search", query), ("pagesize", &limit.to_string())])
.send()
.map_err(AppImageHubSearchError::Transport)?
.error_for_status()
.map_err(AppImageHubSearchError::Transport)?
.text()
.map_err(AppImageHubSearchError::Transport)?;
parse_search_xml(&xml)
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct FixtureAppImageHubTransport;
impl AppImageHubTransport for FixtureAppImageHubTransport {
fn fetch_item(&self, id: &str) -> Result<AppImageHubItem, AppImageHubError> {
fixture_item(id).ok_or_else(|| AppImageHubError::FixtureItemMissing(id.to_owned()))
}
fn search_items(
&self,
query: &str,
limit: usize,
) -> Result<Vec<AppImageHubSearchHit>, AppImageHubSearchError> {
Ok(fixture_search_results(query, limit))
}
}
#[derive(Debug)]
pub enum AppImageHubError {
FixtureItemMissing(String),
InsecureDownloadUrl(String),
Parse(quick_xml::DeError),
Transport(reqwest::Error),
UnsupportedSource(String),
}
#[derive(Debug)]
pub enum AppImageHubSearchError {
Parse(quick_xml::DeError),
Transport(reqwest::Error),
}
#[derive(serde::Deserialize)]
struct OcsSingleResponse {
data: OcsSingleData,
}
#[derive(serde::Deserialize)]
struct OcsSingleData {
content: OcsContent,
}
#[derive(serde::Deserialize)]
struct OcsSearchResponse {
data: OcsSearchData,
}
#[derive(serde::Deserialize)]
struct OcsSearchData {
#[serde(default)]
content: Vec<OcsContent>,
}
#[derive(serde::Deserialize)]
struct OcsContent {
id: String,
name: String,
version: Option<String>,
summary: Option<String>,
detailpage: Option<String>,
tags: Option<String>,
downloadlink1: Option<String>,
downloadname1: Option<String>,
download_package_type1: Option<String>,
download_package_arch1: Option<String>,
downloadmd5sum1: Option<String>,
download_version1: Option<String>,
downloadlink2: Option<String>,
downloadname2: Option<String>,
download_package_type2: Option<String>,
download_package_arch2: Option<String>,
downloadmd5sum2: Option<String>,
download_version2: Option<String>,
downloadlink3: Option<String>,
downloadname3: Option<String>,
download_package_type3: Option<String>,
download_package_arch3: Option<String>,
downloadmd5sum3: Option<String>,
download_version3: Option<String>,
}
fn parse_item_xml(xml: &str) -> Result<AppImageHubItem, AppImageHubError> {
let parsed =
quick_xml::de::from_str::<OcsSingleResponse>(xml).map_err(AppImageHubError::Parse)?;
Ok(content_to_item(parsed.data.content))
}
fn parse_search_xml(xml: &str) -> Result<Vec<AppImageHubSearchHit>, AppImageHubSearchError> {
if !xml.contains("<id>") {
return Ok(Vec::new());
}
let parsed =
quick_xml::de::from_str::<OcsSearchResponse>(xml).map_err(AppImageHubSearchError::Parse)?;
Ok(parsed
.data
.content
.into_iter()
.map(|content| AppImageHubSearchHit {
id: content.id,
name: content.name,
version: normalize_version_text(content.version.as_deref()),
summary: content.summary,
detail_page: content
.detailpage
.unwrap_or_else(|| "https://www.appimagehub.com".to_owned()),
tags: split_tags(content.tags.as_deref()),
})
.collect())
}
fn content_to_item(content: OcsContent) -> AppImageHubItem {
let detail_page = content
.detailpage
.clone()
.unwrap_or_else(|| "https://www.appimagehub.com".to_owned());
let summary = content.summary.clone();
let tags = split_tags(content.tags.as_deref());
let downloads = collect_downloads(&content);
AppImageHubItem {
id: content.id,
name: content.name,
version: normalize_version_text(content.version.as_deref()),
summary,
detail_page,
tags,
downloads,
}
}
fn validate_download_url(url: &str) -> Result<(), AppImageHubError> {
if !url.starts_with("https://") {
return Err(AppImageHubError::InsecureDownloadUrl(url.to_owned()));
}
Ok(())
}
fn collect_downloads(content: &OcsContent) -> Vec<AppImageHubDownload> {
let mut downloads = Vec::new();
for download in [
download_slot(
content.downloadlink1.as_deref(),
content.downloadname1.as_deref(),
content.download_package_type1.as_deref(),
content.download_package_arch1.as_deref(),
content.downloadmd5sum1.as_deref(),
content.download_version1.as_deref(),
),
download_slot(
content.downloadlink2.as_deref(),
content.downloadname2.as_deref(),
content.download_package_type2.as_deref(),
content.download_package_arch2.as_deref(),
content.downloadmd5sum2.as_deref(),
content.download_version2.as_deref(),
),
download_slot(
content.downloadlink3.as_deref(),
content.downloadname3.as_deref(),
content.download_package_type3.as_deref(),
content.download_package_arch3.as_deref(),
content.downloadmd5sum3.as_deref(),
content.download_version3.as_deref(),
),
]
.into_iter()
.flatten()
{
downloads.push(download);
}
downloads
}
fn download_slot(
link: Option<&str>,
name: Option<&str>,
package_type: Option<&str>,
arch: Option<&str>,
md5sum: Option<&str>,
version: Option<&str>,
) -> Option<AppImageHubDownload> {
let url = link?.trim();
if url.is_empty() {
return None;
}
Some(AppImageHubDownload {
url: url.to_owned(),
name: name.unwrap_or("download").trim().to_owned(),
package_type: trim_optional(package_type),
arch: trim_optional(arch),
md5sum: trim_optional(md5sum),
version: trim_optional(version),
})
}
fn trim_optional(value: Option<&str>) -> Option<String> {
value
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
}
fn normalize_version_text(value: Option<&str>) -> String {
let value = value.map(str::trim).filter(|value| !value.is_empty());
match value {
Some("Latest") | Some("latest") | None => "latest".to_owned(),
Some(other) => other.to_owned(),
}
}
fn split_tags(tags: Option<&str>) -> Vec<String> {
tags.unwrap_or_default()
.split(',')
.map(str::trim)
.filter(|tag| !tag.is_empty())
.map(ToOwned::to_owned)
.collect()
}
fn source_id(source: &SourceRef) -> Result<&str, AppImageHubError> {
source
.canonical_locator
.as_deref()
.or_else(|| source.locator.rsplit('/').next())
.filter(|value| !value.is_empty())
.ok_or_else(|| AppImageHubError::UnsupportedSource(source.locator.clone()))
}
fn is_appimage_download(download: &AppImageHubDownload) -> bool {
download
.package_type
.as_deref()
.map(|kind| kind.eq_ignore_ascii_case("appimage"))
.unwrap_or(false)
|| download.name.ends_with(".AppImage")
}
fn resolved_version(item: &AppImageHubItem, download: &AppImageHubDownload) -> String {
download
.version
.as_deref()
.map(|value| normalize_version_text(Some(value)))
.filter(|value| value != "latest")
.unwrap_or_else(|| item.version.clone())
}
fn fixture_item(id: &str) -> Option<AppImageHubItem> {
let insecure_http = env::var("UPM_APPIMAGEHUB_FIXTURE_INSECURE_HTTP")
.ok()
.as_deref()
== Some("1");
let bad_md5 = env::var("UPM_APPIMAGEHUB_FIXTURE_BAD_MD5").ok().as_deref() == Some("1");
match id {
"2338455" => Some(AppImageHubItem {
id: "2338455".to_owned(),
name: "Firefox by Mozilla - Official AppImage Edition".to_owned(),
version: "latest".to_owned(),
summary: Some("Take control of your internet with the Firefox browser".to_owned()),
detail_page: "https://www.appimagehub.com/p/2338455".to_owned(),
tags: vec![
"appimage".to_owned(),
"x86-64".to_owned(),
"desktop".to_owned(),
"release-stable".to_owned(),
],
downloads: vec![AppImageHubDownload {
url: if insecure_http {
"http://files06.pling.com/api/files/download/firefox-x86-64.AppImage".to_owned()
} else {
"https://files06.pling.com/api/files/download/firefox-x86-64.AppImage"
.to_owned()
},
name: "firefox-x86-64.AppImage".to_owned(),
package_type: Some("appimage".to_owned()),
arch: Some("x86-64".to_owned()),
md5sum: Some(if bad_md5 {
"00000000000000000000000000000000".to_owned()
} else {
"2a685cf45213d5a2a243273fa68dafa6".to_owned()
}),
version: None,
}],
}),
"2337998" => Some(AppImageHubItem {
id: "2337998".to_owned(),
name: "Example Non-AppImage Package".to_owned(),
version: "latest".to_owned(),
summary: Some("An item that does not expose an AppImage download".to_owned()),
detail_page: "https://www.appimagehub.com/p/2337998".to_owned(),
tags: vec!["desktop".to_owned()],
downloads: vec![AppImageHubDownload {
url: "https://files06.pling.com/api/files/download/example.deb".to_owned(),
name: "example.deb".to_owned(),
package_type: Some("debian-package".to_owned()),
arch: Some("x86-64".to_owned()),
md5sum: None,
version: Some("2.1.1".to_owned()),
}],
}),
_ => None,
}
}
fn fixture_search_results(query: &str, limit: usize) -> Vec<AppImageHubSearchHit> {
let query = query.trim().to_ascii_lowercase();
let fixtures = [
AppImageHubSearchHit {
id: "2338455".to_owned(),
name: "Firefox by Mozilla - Official AppImage Edition".to_owned(),
version: "latest".to_owned(),
summary: Some("Take control of your internet with the Firefox browser".to_owned()),
detail_page: "https://www.appimagehub.com/p/2338455".to_owned(),
tags: vec!["browser".to_owned(), "appimage".to_owned()],
},
AppImageHubSearchHit {
id: "2338484".to_owned(),
name: "Waterfox".to_owned(),
version: "latest".to_owned(),
summary: Some("Open Source, Private Browsing".to_owned()),
detail_page: "https://www.appimagehub.com/p/2338484".to_owned(),
tags: vec!["browser".to_owned(), "appimage".to_owned()],
},
];
fixtures
.into_iter()
.filter(|item| {
item.name.to_ascii_lowercase().contains(&query)
|| item
.tags
.iter()
.any(|tag| tag.to_ascii_lowercase().contains(&query))
})
.take(limit)
.collect()
}

View file

@ -0,0 +1 @@
pub mod appimagehub;

View file

@ -0,0 +1,149 @@
use upm_appimage::add::{AppImageHubAdapter, AppImageHubAddProvider};
use upm_appimage::search::AppImageHubSearchProvider;
use upm_appimage::source::appimagehub::FixtureAppImageHubTransport;
use upm_core::adapters::traits::AdapterResolveOutcome;
use upm_core::app::providers::ExternalAddProvider;
use upm_core::app::query::resolve_query;
use upm_core::app::search::{
GitHubSearchProvider, SearchProvider, SearchProviderError, build_search_results_with,
};
use upm_core::domain::app::AppRecord;
use upm_core::domain::search::{SearchInstallStatus, SearchQuery, SearchResult};
use upm_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind, SourceRef};
use upm_core::source::github::FixtureGitHubTransport;
struct StubProvider {
hit: SearchResult,
}
impl SearchProvider for StubProvider {
fn search(&self, _query: &SearchQuery) -> Result<Vec<SearchResult>, SearchProviderError> {
Ok(vec![self.hit.clone()])
}
}
#[test]
fn appimagehub_search_provider_maps_hits_to_install_ready_results() {
let provider = AppImageHubSearchProvider::new(&FixtureAppImageHubTransport);
let results = provider.search(&SearchQuery::new("firefox")).unwrap();
assert!(results.iter().any(|hit| {
hit.provider_id == "appimagehub"
&& hit.display_name == "Firefox by Mozilla - Official AppImage Edition"
&& hit.install_query == "appimagehub/2338455"
&& hit.canonical_locator == "2338455"
}));
}
#[test]
fn appimagehub_hits_are_annotated_as_installed_by_canonical_id() {
let provider = AppImageHubSearchProvider::new(&FixtureAppImageHubTransport);
let installed = vec![AppRecord {
stable_id: "firefox".to_owned(),
display_name: "Firefox by Mozilla - Official AppImage Edition".to_owned(),
source_input: Some("appimagehub/2338455".to_owned()),
source: Some(SourceRef {
kind: SourceKind::AppImageHub,
locator: "https://www.appimagehub.com/p/2338455".to_owned(),
input_kind: SourceInputKind::AppImageHubShorthand,
normalized_kind: NormalizedSourceKind::AppImageHub,
canonical_locator: Some("2338455".to_owned()),
requested_tag: None,
requested_asset_name: None,
tracks_latest: true,
}),
installed_version: Some("latest".to_owned()),
update_strategy: None,
metadata: Vec::new(),
install: None,
}];
let results =
build_search_results_with(&SearchQuery::new("firefox"), &installed, &[&provider]).unwrap();
assert!(results.remote_hits.iter().any(|hit| {
hit.canonical_locator == "2338455"
&& matches!(
hit.install_status,
SearchInstallStatus::Installed {
installed_version: Some(ref version)
} if version == "latest"
)
}));
}
#[test]
fn search_can_merge_github_and_appimagehub_providers() {
let github = GitHubSearchProvider::new(&FixtureGitHubTransport);
let appimagehub = AppImageHubSearchProvider::new(&FixtureAppImageHubTransport);
let stub = StubProvider {
hit: SearchResult {
provider_id: "github".to_owned(),
display_name: "firefox-tooling/firestarter".to_owned(),
description: Some("Stub GitHub result".to_owned()),
source_locator: "https://github.com/firefox-tooling/firestarter".to_owned(),
install_query: "firefox-tooling/firestarter".to_owned(),
canonical_locator: "firefox-tooling/firestarter".to_owned(),
version: Some("1.0.0".to_owned()),
install_status: SearchInstallStatus::Available,
},
};
let results = build_search_results_with(
&SearchQuery::new("firefox"),
&[],
&[&stub, &github, &appimagehub],
)
.unwrap();
assert!(
results
.remote_hits
.iter()
.any(|hit| hit.provider_id == "github")
);
assert!(
results
.remote_hits
.iter()
.any(|hit| hit.provider_id == "appimagehub")
);
}
#[test]
fn appimagehub_adapter_resolves_installable_items_through_fixture_transport() {
let adapter = AppImageHubAdapter;
let source = resolve_query("appimagehub/2338455").unwrap();
let resolution = adapter
.resolve_source_with(&source, &FixtureAppImageHubTransport)
.unwrap();
assert!(matches!(
resolution,
AdapterResolveOutcome::Resolved(resolution)
if resolution.source.kind == SourceKind::AppImageHub
&& resolution.source.canonical_locator.as_deref() == Some("2338455")
&& resolution.release.version == "latest"
));
}
#[test]
fn appimagehub_add_provider_resolves_external_add_plan() {
let provider = AppImageHubAddProvider::new(&FixtureAppImageHubTransport);
let source = resolve_query("appimagehub/2338455").unwrap();
let resolution = provider.resolve(&source).unwrap().unwrap();
assert_eq!(resolution.resolution.source.kind, SourceKind::AppImageHub);
assert_eq!(resolution.resolution.release.version, "latest");
assert_eq!(
resolution.selected_artifact.url,
"https://files06.pling.com/api/files/download/firefox-x86-64.AppImage"
);
assert_eq!(
resolution.display_name_hint.as_deref(),
Some("Firefox by Mozilla - Official AppImage Edition")
);
}