Merge branch 'feat/update-execution-and-provider-contract'
This commit is contained in:
commit
d1b9754c0b
16 changed files with 407 additions and 79 deletions
|
|
@ -18,8 +18,7 @@ pub struct Cli {
|
||||||
|
|
||||||
impl Cli {
|
impl Cli {
|
||||||
pub fn is_review_update_flow(&self) -> bool {
|
pub fn is_review_update_flow(&self) -> bool {
|
||||||
matches!(self.command, Some(Command::Update))
|
self.command.is_none() && self.query.is_none()
|
||||||
|| (self.command.is_none() && self.query.is_none())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,9 @@ use aim_core::app::add::{
|
||||||
};
|
};
|
||||||
use aim_core::app::list::{ListRow, build_list_rows};
|
use aim_core::app::list::{ListRow, build_list_rows};
|
||||||
use aim_core::app::remove::{RemovalResult, remove_registered_app};
|
use aim_core::app::remove::{RemovalResult, remove_registered_app};
|
||||||
use aim_core::app::update::build_update_plan;
|
use aim_core::app::update::{build_update_plan, execute_updates};
|
||||||
use aim_core::domain::app::AppRecord;
|
use aim_core::domain::app::AppRecord;
|
||||||
use aim_core::domain::update::UpdatePlan;
|
use aim_core::domain::update::{UpdateExecutionResult, UpdatePlan};
|
||||||
use aim_core::registry::model::Registry;
|
use aim_core::registry::model::Registry;
|
||||||
use aim_core::registry::store::RegistryStore;
|
use aim_core::registry::store::RegistryStore;
|
||||||
|
|
||||||
|
|
@ -44,7 +44,15 @@ pub fn dispatch(cli: Cli) -> Result<DispatchResult, DispatchError> {
|
||||||
})?;
|
})?;
|
||||||
Ok(DispatchResult::Removed(Box::new(removal)))
|
Ok(DispatchResult::Removed(Box::new(removal)))
|
||||||
}
|
}
|
||||||
cli::args::Command::Update => Ok(DispatchResult::UpdatePlan(build_update_plan(&apps)?)),
|
cli::args::Command::Update => {
|
||||||
|
let updates = execute_updates(&apps, &install_home)?;
|
||||||
|
let updated_apps = updates.apps.clone();
|
||||||
|
store.save(&Registry {
|
||||||
|
version: registry.version,
|
||||||
|
apps: updated_apps,
|
||||||
|
})?;
|
||||||
|
Ok(DispatchResult::Updated(Box::new(updates)))
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -94,6 +102,7 @@ pub enum DispatchResult {
|
||||||
PendingAdd(Box<AddPlan>),
|
PendingAdd(Box<AddPlan>),
|
||||||
Removed(Box<RemovalResult>),
|
Removed(Box<RemovalResult>),
|
||||||
UpdatePlan(UpdatePlan),
|
UpdatePlan(UpdatePlan),
|
||||||
|
Updated(Box<UpdateExecutionResult>),
|
||||||
Noop,
|
Noop,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -105,6 +114,7 @@ pub enum DispatchError {
|
||||||
RemovePlan(aim_core::app::remove::RemoveRegisteredAppError),
|
RemovePlan(aim_core::app::remove::RemoveRegisteredAppError),
|
||||||
Registry(aim_core::registry::store::RegistryStoreError),
|
Registry(aim_core::registry::store::RegistryStoreError),
|
||||||
UpdatePlan(aim_core::app::update::BuildUpdatePlanError),
|
UpdatePlan(aim_core::app::update::BuildUpdatePlanError),
|
||||||
|
UpdateExecution(aim_core::app::update::ExecuteUpdatesError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<aim_core::app::add::BuildAddPlanError> for DispatchError {
|
impl From<aim_core::app::add::BuildAddPlanError> for DispatchError {
|
||||||
|
|
@ -131,6 +141,12 @@ impl From<aim_core::app::update::BuildUpdatePlanError> for DispatchError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<aim_core::app::update::ExecuteUpdatesError> for DispatchError {
|
||||||
|
fn from(value: aim_core::app::update::ExecuteUpdatesError) -> Self {
|
||||||
|
Self::UpdateExecution(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<aim_core::app::remove::RemoveRegisteredAppError> for DispatchError {
|
impl From<aim_core::app::remove::RemoveRegisteredAppError> for DispatchError {
|
||||||
fn from(value: aim_core::app::remove::RemoveRegisteredAppError) -> Self {
|
fn from(value: aim_core::app::remove::RemoveRegisteredAppError) -> Self {
|
||||||
Self::RemovePlan(value)
|
Self::RemovePlan(value)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use aim_core::app::add::AddPlan;
|
use aim_core::app::add::AddPlan;
|
||||||
|
use aim_core::domain::update::UpdateExecutionStatus;
|
||||||
|
|
||||||
use crate::DispatchResult;
|
use crate::DispatchResult;
|
||||||
|
|
||||||
|
|
@ -15,6 +16,7 @@ pub fn render_dispatch_result(result: &DispatchResult) -> String {
|
||||||
DispatchResult::UpdatePlan(plan) => {
|
DispatchResult::UpdatePlan(plan) => {
|
||||||
render_update_summary(plan.items.len(), plan.items.len(), 0)
|
render_update_summary(plan.items.len(), plan.items.len(), 0)
|
||||||
}
|
}
|
||||||
|
DispatchResult::Updated(result) => render_updated_apps(result),
|
||||||
DispatchResult::Noop => String::new(),
|
DispatchResult::Noop => String::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -88,3 +90,29 @@ fn render_removed_app(removed: &aim_core::app::remove::RemovalResult) -> String
|
||||||
format!("{summary}\n{warning_lines}")
|
format!("{summary}\n{warning_lines}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_updated_apps(result: &aim_core::domain::update::UpdateExecutionResult) -> String {
|
||||||
|
let mut lines = vec![format!(
|
||||||
|
"updated apps: {}, failed: {}",
|
||||||
|
result.updated_count(),
|
||||||
|
result.failed_count()
|
||||||
|
)];
|
||||||
|
|
||||||
|
for item in &result.items {
|
||||||
|
match &item.status {
|
||||||
|
UpdateExecutionStatus::Updated => lines.push(format!(
|
||||||
|
"updated: {} ({}) {} -> {}",
|
||||||
|
item.display_name,
|
||||||
|
item.stable_id,
|
||||||
|
item.from_version.as_deref().unwrap_or("unknown"),
|
||||||
|
item.to_version.as_deref().unwrap_or("unknown")
|
||||||
|
)),
|
||||||
|
UpdateExecutionStatus::Failed { reason } => lines.push(format!(
|
||||||
|
"failed: {} ({}) {}",
|
||||||
|
item.display_name, item.stable_id, reason
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.join("\n")
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
|
use aim_core::domain::app::{AppRecord, InstallMetadata, InstallScope};
|
||||||
|
use aim_core::registry::model::Registry;
|
||||||
|
use aim_core::registry::store::RegistryStore;
|
||||||
use assert_cmd::Command;
|
use assert_cmd::Command;
|
||||||
|
use predicates::prelude::PredicateBooleanExt;
|
||||||
use predicates::str::contains;
|
use predicates::str::contains;
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
|
@ -179,3 +183,49 @@ fn system_request_on_immutable_host_falls_back_to_user_install() {
|
||||||
.stdout(contains("installing as user"))
|
.stdout(contains("installing as user"))
|
||||||
.stdout(contains("downgraded to user scope"));
|
.stdout(contains("downgraded to user scope"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn update_command_applies_updates() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let registry_path = dir.path().join("registry.toml");
|
||||||
|
let payload_path = dir
|
||||||
|
.path()
|
||||||
|
.join("install-home/.local/lib/aim/appimages/pingdotgg-t3code.AppImage");
|
||||||
|
let store = RegistryStore::new(registry_path.clone());
|
||||||
|
store
|
||||||
|
.save(&Registry {
|
||||||
|
version: 1,
|
||||||
|
apps: vec![AppRecord {
|
||||||
|
stable_id: "pingdotgg-t3code".to_owned(),
|
||||||
|
display_name: "t3code".to_owned(),
|
||||||
|
source_input: Some("pingdotgg/t3code".to_owned()),
|
||||||
|
source: None,
|
||||||
|
installed_version: Some("0.0.11".to_owned()),
|
||||||
|
update_strategy: None,
|
||||||
|
metadata: Vec::new(),
|
||||||
|
install: Some(InstallMetadata {
|
||||||
|
scope: InstallScope::User,
|
||||||
|
payload_path: None,
|
||||||
|
desktop_entry_path: None,
|
||||||
|
icon_path: None,
|
||||||
|
}),
|
||||||
|
}],
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut cmd = Command::cargo_bin("aim").unwrap();
|
||||||
|
|
||||||
|
cmd.arg("update")
|
||||||
|
.env("AIM_REGISTRY_PATH", ®istry_path)
|
||||||
|
.env(FIXTURE_MODE_ENV, "1")
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(contains("updated apps: 1"))
|
||||||
|
.stdout(contains("updates found:").not());
|
||||||
|
|
||||||
|
let updated = store.load().unwrap();
|
||||||
|
assert_eq!(updated.apps.len(), 1);
|
||||||
|
assert_eq!(updated.apps[0].stable_id, "pingdotgg-t3code");
|
||||||
|
assert_eq!(updated.apps[0].installed_version.as_deref(), Some("0.0.12"));
|
||||||
|
assert!(payload_path.exists());
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
use crate::adapters::traits::{AdapterCapabilities, SourceAdapter};
|
use crate::adapters::traits::{
|
||||||
|
AdapterCapabilities, AdapterError, AdapterResolution, SourceAdapter,
|
||||||
|
};
|
||||||
|
use crate::domain::source::SourceRef;
|
||||||
|
|
||||||
pub struct CustomJsonAdapter;
|
pub struct CustomJsonAdapter;
|
||||||
|
|
||||||
|
|
@ -10,4 +13,12 @@ impl SourceAdapter for CustomJsonAdapter {
|
||||||
fn capabilities(&self) -> AdapterCapabilities {
|
fn capabilities(&self) -> AdapterCapabilities {
|
||||||
AdapterCapabilities::exact_resolution_only()
|
AdapterCapabilities::exact_resolution_only()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn normalize(&self, _query: &str) -> Result<SourceRef, AdapterError> {
|
||||||
|
Err(AdapterError::UnsupportedQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve(&self, _source: &SourceRef) -> Result<AdapterResolution, AdapterError> {
|
||||||
|
Err(AdapterError::UnsupportedSource)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,32 @@
|
||||||
use crate::adapters::traits::{AdapterCapabilities, AdapterResolution, SourceAdapter};
|
use crate::adapters::traits::{
|
||||||
|
AdapterCapabilities, AdapterError, AdapterResolution, SourceAdapter,
|
||||||
|
};
|
||||||
|
use crate::app::query::resolve_query;
|
||||||
use crate::domain::source::{ResolvedRelease, SourceKind, SourceRef};
|
use crate::domain::source::{ResolvedRelease, SourceKind, SourceRef};
|
||||||
|
|
||||||
pub struct DirectUrlAdapter;
|
pub struct DirectUrlAdapter;
|
||||||
|
|
||||||
impl DirectUrlAdapter {
|
impl SourceAdapter for DirectUrlAdapter {
|
||||||
pub fn resolve(&self, source: &SourceRef) -> Result<AdapterResolution, DirectUrlAdapterError> {
|
fn id(&self) -> &'static str {
|
||||||
|
"direct-url"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn capabilities(&self) -> AdapterCapabilities {
|
||||||
|
AdapterCapabilities::exact_resolution_only()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize(&self, query: &str) -> Result<SourceRef, AdapterError> {
|
||||||
|
let source = resolve_query(query).map_err(|_| AdapterError::UnsupportedQuery)?;
|
||||||
if source.kind != SourceKind::DirectUrl {
|
if source.kind != SourceKind::DirectUrl {
|
||||||
return Err(DirectUrlAdapterError::UnsupportedSource);
|
return Err(AdapterError::UnsupportedQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(source)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve(&self, source: &SourceRef) -> Result<AdapterResolution, AdapterError> {
|
||||||
|
if source.kind != SourceKind::DirectUrl {
|
||||||
|
return Err(AdapterError::UnsupportedSource);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(AdapterResolution {
|
Ok(AdapterResolution {
|
||||||
|
|
@ -18,18 +38,3 @@ impl DirectUrlAdapter {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SourceAdapter for DirectUrlAdapter {
|
|
||||||
fn id(&self) -> &'static str {
|
|
||||||
"direct-url"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn capabilities(&self) -> AdapterCapabilities {
|
|
||||||
AdapterCapabilities::exact_resolution_only()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq)]
|
|
||||||
pub enum DirectUrlAdapterError {
|
|
||||||
UnsupportedSource,
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
use crate::adapters::traits::{AdapterCapabilities, AdapterResolution, SourceAdapter};
|
use crate::adapters::traits::{
|
||||||
|
AdapterCapabilities, AdapterError, AdapterResolution, SourceAdapter,
|
||||||
|
};
|
||||||
use crate::app::query::resolve_query;
|
use crate::app::query::resolve_query;
|
||||||
use crate::domain::source::{ResolvedRelease, SourceKind, SourceRef};
|
use crate::domain::source::{ResolvedRelease, SourceKind, SourceRef};
|
||||||
|
|
||||||
|
|
@ -14,24 +16,6 @@ impl GitHubAdapter {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self
|
Self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resolve(&self, source: &SourceRef) -> Result<AdapterResolution, GitHubAdapterError> {
|
|
||||||
if source.kind != SourceKind::GitHub {
|
|
||||||
return Err(GitHubAdapterError::UnsupportedSource);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(AdapterResolution {
|
|
||||||
source: source.clone(),
|
|
||||||
release: ResolvedRelease {
|
|
||||||
version: "latest".to_owned(),
|
|
||||||
prerelease: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn normalize(&self, query: &str) -> Result<SourceRef, GitHubAdapterError> {
|
|
||||||
resolve_query(query).map_err(|_| GitHubAdapterError::UnsupportedSource)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SourceAdapter for GitHubAdapter {
|
impl SourceAdapter for GitHubAdapter {
|
||||||
|
|
@ -45,9 +29,27 @@ impl SourceAdapter for GitHubAdapter {
|
||||||
supports_exact_resolution: true,
|
supports_exact_resolution: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq)]
|
fn normalize(&self, query: &str) -> Result<SourceRef, AdapterError> {
|
||||||
pub enum GitHubAdapterError {
|
let source = resolve_query(query).map_err(|_| AdapterError::UnsupportedQuery)?;
|
||||||
UnsupportedSource,
|
if source.kind != SourceKind::GitHub {
|
||||||
|
return Err(AdapterError::UnsupportedQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(source)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve(&self, source: &SourceRef) -> Result<AdapterResolution, AdapterError> {
|
||||||
|
if source.kind != SourceKind::GitHub {
|
||||||
|
return Err(AdapterError::UnsupportedSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(AdapterResolution {
|
||||||
|
source: source.clone(),
|
||||||
|
release: ResolvedRelease {
|
||||||
|
version: "latest".to_owned(),
|
||||||
|
prerelease: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,11 @@
|
||||||
use crate::adapters::traits::{AdapterCapabilities, AdapterResolution, SourceAdapter};
|
use crate::adapters::traits::{
|
||||||
|
AdapterCapabilities, AdapterError, AdapterResolution, SourceAdapter,
|
||||||
|
};
|
||||||
|
use crate::app::query::resolve_query;
|
||||||
use crate::domain::source::{ResolvedRelease, SourceKind, SourceRef};
|
use crate::domain::source::{ResolvedRelease, SourceKind, SourceRef};
|
||||||
|
|
||||||
pub struct GitLabAdapter;
|
pub struct GitLabAdapter;
|
||||||
|
|
||||||
impl GitLabAdapter {
|
|
||||||
pub fn resolve(&self, source: &SourceRef) -> Result<AdapterResolution, GitLabAdapterError> {
|
|
||||||
if source.kind != SourceKind::GitLab {
|
|
||||||
return Err(GitLabAdapterError::UnsupportedSource);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(AdapterResolution {
|
|
||||||
source: source.clone(),
|
|
||||||
release: ResolvedRelease {
|
|
||||||
version: "latest".to_owned(),
|
|
||||||
prerelease: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SourceAdapter for GitLabAdapter {
|
impl SourceAdapter for GitLabAdapter {
|
||||||
fn id(&self) -> &'static str {
|
fn id(&self) -> &'static str {
|
||||||
"gitlab"
|
"gitlab"
|
||||||
|
|
@ -30,9 +17,27 @@ impl SourceAdapter for GitLabAdapter {
|
||||||
supports_exact_resolution: true,
|
supports_exact_resolution: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq)]
|
fn normalize(&self, query: &str) -> Result<SourceRef, AdapterError> {
|
||||||
pub enum GitLabAdapterError {
|
let source = resolve_query(query).map_err(|_| AdapterError::UnsupportedQuery)?;
|
||||||
UnsupportedSource,
|
if source.kind != SourceKind::GitLab {
|
||||||
|
return Err(AdapterError::UnsupportedQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(source)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve(&self, source: &SourceRef) -> Result<AdapterResolution, AdapterError> {
|
||||||
|
if source.kind != SourceKind::GitLab {
|
||||||
|
return Err(AdapterError::UnsupportedSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(AdapterResolution {
|
||||||
|
source: source.clone(),
|
||||||
|
release: ResolvedRelease {
|
||||||
|
version: "latest".to_owned(),
|
||||||
|
prerelease: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
use crate::adapters::traits::{AdapterCapabilities, SourceAdapter};
|
use crate::adapters::traits::{
|
||||||
|
AdapterCapabilities, AdapterError, AdapterResolution, SourceAdapter,
|
||||||
|
};
|
||||||
|
use crate::domain::source::SourceRef;
|
||||||
|
|
||||||
pub struct SourceForgeAdapter;
|
pub struct SourceForgeAdapter;
|
||||||
|
|
||||||
|
|
@ -13,4 +16,12 @@ impl SourceAdapter for SourceForgeAdapter {
|
||||||
supports_exact_resolution: true,
|
supports_exact_resolution: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn normalize(&self, _query: &str) -> Result<SourceRef, AdapterError> {
|
||||||
|
Err(AdapterError::UnsupportedQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve(&self, _source: &SourceRef) -> Result<AdapterResolution, AdapterError> {
|
||||||
|
Err(AdapterError::UnsupportedSource)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
use crate::adapters::traits::AdapterCapabilities;
|
use crate::adapters::traits::{
|
||||||
use crate::adapters::traits::SourceAdapter;
|
AdapterCapabilities, AdapterError, AdapterResolution, SourceAdapter,
|
||||||
|
};
|
||||||
|
use crate::domain::source::SourceRef;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct MockAdapter {
|
pub struct MockAdapter {
|
||||||
|
|
@ -24,4 +26,12 @@ impl SourceAdapter for MockAdapter {
|
||||||
fn capabilities(&self) -> AdapterCapabilities {
|
fn capabilities(&self) -> AdapterCapabilities {
|
||||||
self.capabilities
|
self.capabilities
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn normalize(&self, _query: &str) -> Result<SourceRef, AdapterError> {
|
||||||
|
Err(AdapterError::UnsupportedQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve(&self, _source: &SourceRef) -> Result<AdapterResolution, AdapterError> {
|
||||||
|
Err(AdapterError::UnsupportedSource)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,19 @@ pub struct AdapterResolution {
|
||||||
pub release: ResolvedRelease,
|
pub release: ResolvedRelease,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum AdapterError {
|
||||||
|
UnsupportedQuery,
|
||||||
|
UnsupportedSource,
|
||||||
|
ResolutionFailed(String),
|
||||||
|
}
|
||||||
|
|
||||||
pub trait SourceAdapter {
|
pub trait SourceAdapter {
|
||||||
fn id(&self) -> &'static str;
|
fn id(&self) -> &'static str;
|
||||||
|
|
||||||
fn capabilities(&self) -> AdapterCapabilities;
|
fn capabilities(&self) -> AdapterCapabilities;
|
||||||
|
|
||||||
|
fn normalize(&self, query: &str) -> Result<SourceRef, AdapterError>;
|
||||||
|
|
||||||
|
fn resolve(&self, source: &SourceRef) -> Result<AdapterResolution, AdapterError>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
use crate::adapters::traits::{AdapterCapabilities, SourceAdapter};
|
use crate::adapters::traits::{
|
||||||
|
AdapterCapabilities, AdapterError, AdapterResolution, SourceAdapter,
|
||||||
|
};
|
||||||
|
use crate::domain::source::SourceRef;
|
||||||
|
|
||||||
pub struct ZsyncAdapter;
|
pub struct ZsyncAdapter;
|
||||||
|
|
||||||
|
|
@ -10,4 +13,12 @@ impl SourceAdapter for ZsyncAdapter {
|
||||||
fn capabilities(&self) -> AdapterCapabilities {
|
fn capabilities(&self) -> AdapterCapabilities {
|
||||||
AdapterCapabilities::exact_resolution_only()
|
AdapterCapabilities::exact_resolution_only()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn normalize(&self, _query: &str) -> Result<SourceRef, AdapterError> {
|
||||||
|
Err(AdapterError::UnsupportedQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve(&self, _source: &SourceRef) -> Result<AdapterResolution, AdapterError> {
|
||||||
|
Err(AdapterError::UnsupportedSource)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
use crate::domain::app::AppRecord;
|
use std::path::Path;
|
||||||
use crate::domain::update::{ChannelPreference, PlannedUpdate, UpdateChannelKind, UpdatePlan};
|
|
||||||
|
use crate::app::add::{build_add_plan, install_app};
|
||||||
|
use crate::domain::app::{AppRecord, InstallScope};
|
||||||
|
use crate::domain::update::{
|
||||||
|
ChannelPreference, ExecutedUpdate, PlannedUpdate, UpdateChannelKind, UpdateExecutionResult,
|
||||||
|
UpdateExecutionStatus, UpdatePlan,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn build_update_plan(apps: &[AppRecord]) -> Result<UpdatePlan, BuildUpdatePlanError> {
|
pub fn build_update_plan(apps: &[AppRecord]) -> Result<UpdatePlan, BuildUpdatePlanError> {
|
||||||
Ok(UpdatePlan {
|
Ok(UpdatePlan {
|
||||||
|
|
@ -7,9 +13,59 @@ pub fn build_update_plan(apps: &[AppRecord]) -> Result<UpdatePlan, BuildUpdatePl
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn execute_updates(
|
||||||
|
apps: &[AppRecord],
|
||||||
|
install_home: &Path,
|
||||||
|
) -> Result<UpdateExecutionResult, ExecuteUpdatesError> {
|
||||||
|
let mut updated_apps = Vec::with_capacity(apps.len());
|
||||||
|
let mut items = Vec::with_capacity(apps.len());
|
||||||
|
|
||||||
|
for app in apps {
|
||||||
|
match execute_update(app, install_home) {
|
||||||
|
Ok(updated) => {
|
||||||
|
let warnings = updated
|
||||||
|
.warnings
|
||||||
|
.iter()
|
||||||
|
.chain(updated.install_outcome.warnings.iter())
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
let record = updated.record;
|
||||||
|
items.push(ExecutedUpdate {
|
||||||
|
stable_id: app.stable_id.clone(),
|
||||||
|
display_name: app.display_name.clone(),
|
||||||
|
from_version: app.installed_version.clone(),
|
||||||
|
to_version: record.installed_version.clone(),
|
||||||
|
warnings,
|
||||||
|
status: UpdateExecutionStatus::Updated,
|
||||||
|
});
|
||||||
|
updated_apps.push(record);
|
||||||
|
}
|
||||||
|
Err(reason) => {
|
||||||
|
items.push(ExecutedUpdate {
|
||||||
|
stable_id: app.stable_id.clone(),
|
||||||
|
display_name: app.display_name.clone(),
|
||||||
|
from_version: app.installed_version.clone(),
|
||||||
|
to_version: app.installed_version.clone(),
|
||||||
|
warnings: Vec::new(),
|
||||||
|
status: UpdateExecutionStatus::Failed { reason },
|
||||||
|
});
|
||||||
|
updated_apps.push(app.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(UpdateExecutionResult {
|
||||||
|
apps: updated_apps,
|
||||||
|
items,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq)]
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
pub enum BuildUpdatePlanError {}
|
pub enum BuildUpdatePlanError {}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
|
pub enum ExecuteUpdatesError {}
|
||||||
|
|
||||||
fn plan_update(app: &AppRecord) -> PlannedUpdate {
|
fn plan_update(app: &AppRecord) -> PlannedUpdate {
|
||||||
let (selected_channel, selection_reason) = if let Some(strategy) = &app.update_strategy {
|
let (selected_channel, selection_reason) = if let Some(strategy) = &app.update_strategy {
|
||||||
if strategy.preferred.locator.contains("fail") {
|
if strategy.preferred.locator.contains("fail") {
|
||||||
|
|
@ -47,3 +103,31 @@ fn plan_update(app: &AppRecord) -> PlannedUpdate {
|
||||||
selection_reason,
|
selection_reason,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn execute_update(
|
||||||
|
app: &AppRecord,
|
||||||
|
install_home: &Path,
|
||||||
|
) -> Result<crate::app::add::InstalledApp, String> {
|
||||||
|
let query = update_query(app).ok_or_else(|| "missing install source".to_owned())?;
|
||||||
|
let requested_scope = app
|
||||||
|
.install
|
||||||
|
.as_ref()
|
||||||
|
.map(|install| install.scope)
|
||||||
|
.unwrap_or(InstallScope::User);
|
||||||
|
let plan = build_add_plan(&query)
|
||||||
|
.map_err(|error| format!("failed to build update plan: {error:?}"))?;
|
||||||
|
|
||||||
|
install_app(&query, &plan, install_home, requested_scope)
|
||||||
|
.map_err(|error| format!("failed to install update: {error:?}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_query(app: &AppRecord) -> Option<String> {
|
||||||
|
app.source_input.clone().or_else(|| {
|
||||||
|
app.source.as_ref().map(|source| {
|
||||||
|
source
|
||||||
|
.canonical_locator
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| source.locator.clone())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
use crate::domain::app::AppRecord;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||||
pub enum ParsedMetadataKind {
|
pub enum ParsedMetadataKind {
|
||||||
Unknown,
|
Unknown,
|
||||||
|
|
@ -98,3 +100,41 @@ pub struct PlannedUpdate {
|
||||||
pub selected_channel: ChannelPreference,
|
pub selected_channel: ChannelPreference,
|
||||||
pub selection_reason: String,
|
pub selection_reason: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct UpdateExecutionResult {
|
||||||
|
pub apps: Vec<AppRecord>,
|
||||||
|
pub items: Vec<ExecutedUpdate>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UpdateExecutionResult {
|
||||||
|
pub fn updated_count(&self) -> usize {
|
||||||
|
self.items
|
||||||
|
.iter()
|
||||||
|
.filter(|item| item.status == UpdateExecutionStatus::Updated)
|
||||||
|
.count()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn failed_count(&self) -> usize {
|
||||||
|
self.items
|
||||||
|
.iter()
|
||||||
|
.filter(|item| matches!(item.status, UpdateExecutionStatus::Failed { .. }))
|
||||||
|
.count()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct ExecutedUpdate {
|
||||||
|
pub stable_id: String,
|
||||||
|
pub display_name: String,
|
||||||
|
pub from_version: Option<String>,
|
||||||
|
pub to_version: Option<String>,
|
||||||
|
pub warnings: Vec<String>,
|
||||||
|
pub status: UpdateExecutionStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum UpdateExecutionStatus {
|
||||||
|
Updated,
|
||||||
|
Failed { reason: String },
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use aim_core::adapters::github::GitHubAdapter;
|
use aim_core::adapters::github::GitHubAdapter;
|
||||||
use aim_core::adapters::traits::AdapterCapabilities;
|
use aim_core::adapters::gitlab::GitLabAdapter;
|
||||||
|
use aim_core::adapters::traits::{AdapterCapabilities, SourceAdapter};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn adapter_capabilities_can_report_exact_resolution_only() {
|
fn adapter_capabilities_can_report_exact_resolution_only() {
|
||||||
|
|
@ -9,10 +10,27 @@ fn adapter_capabilities_can_report_exact_resolution_only() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn legacy_github_adapter_delegates_to_source_pipeline() {
|
fn legacy_github_adapter_delegates_to_source_pipeline() {
|
||||||
let adapter = GitHubAdapter;
|
let adapter: &dyn SourceAdapter = &GitHubAdapter;
|
||||||
|
|
||||||
let result = adapter.normalize("sharkdp/bat").unwrap();
|
let result = adapter.normalize("sharkdp/bat").unwrap();
|
||||||
|
|
||||||
assert_eq!(result.normalized_kind.as_str(), "github-repository");
|
assert_eq!(result.normalized_kind.as_str(), "github-repository");
|
||||||
assert_eq!(result.canonical_locator.as_deref(), Some("sharkdp/bat"));
|
assert_eq!(result.canonical_locator.as_deref(), Some("sharkdp/bat"));
|
||||||
|
|
||||||
|
let resolution = adapter.resolve(&result).unwrap();
|
||||||
|
assert_eq!(resolution.release.version, "latest");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn gitlab_adapter_normalizes_and_resolves_through_trait() {
|
||||||
|
let adapter: &dyn SourceAdapter = &GitLabAdapter;
|
||||||
|
|
||||||
|
let result = adapter
|
||||||
|
.normalize("https://gitlab.com/example/team/app")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result.kind.as_str(), "gitlab");
|
||||||
|
|
||||||
|
let resolution = adapter.resolve(&result).unwrap();
|
||||||
|
assert_eq!(resolution.release.version, "latest");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use aim_core::app::update::build_update_plan;
|
use aim_core::app::update::{build_update_plan, execute_updates};
|
||||||
use aim_core::domain::app::AppRecord;
|
use aim_core::domain::app::{AppRecord, InstallMetadata, InstallScope};
|
||||||
use aim_core::domain::update::{ChannelPreference, UpdateChannelKind, UpdateStrategy};
|
use aim_core::domain::update::{ChannelPreference, UpdateChannelKind, UpdateStrategy};
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_registry_produces_empty_plan() {
|
fn empty_registry_produces_empty_plan() {
|
||||||
|
|
@ -61,3 +62,29 @@ fn update_plan_uses_alternate_channel_after_preferred_failure() {
|
||||||
);
|
);
|
||||||
assert_eq!(plan.items[0].selection_reason, "preferred-channel-failed");
|
assert_eq!(plan.items[0].selection_reason, "preferred-channel-failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn failed_update_keeps_previous_app_record() {
|
||||||
|
let install_home = tempdir().unwrap();
|
||||||
|
let previous = AppRecord {
|
||||||
|
stable_id: "legacy-bat".to_owned(),
|
||||||
|
display_name: "Legacy Bat".to_owned(),
|
||||||
|
source_input: None,
|
||||||
|
source: None,
|
||||||
|
installed_version: Some("0.9.0".to_owned()),
|
||||||
|
update_strategy: None,
|
||||||
|
metadata: Vec::new(),
|
||||||
|
install: Some(InstallMetadata {
|
||||||
|
scope: InstallScope::User,
|
||||||
|
payload_path: None,
|
||||||
|
desktop_entry_path: None,
|
||||||
|
icon_path: None,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = execute_updates(std::slice::from_ref(&previous), install_home.path()).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result.apps, vec![previous]);
|
||||||
|
assert_eq!(result.updated_count(), 0);
|
||||||
|
assert_eq!(result.failed_count(), 1);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue