Implement update execution and provider contract
This commit is contained in:
parent
842c390260
commit
d8eb7b3cab
16 changed files with 407 additions and 79 deletions
|
|
@ -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;
|
||||
|
||||
|
|
@ -10,4 +13,12 @@ impl SourceAdapter for CustomJsonAdapter {
|
|||
fn capabilities(&self) -> AdapterCapabilities {
|
||||
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};
|
||||
|
||||
pub struct DirectUrlAdapter;
|
||||
|
||||
impl DirectUrlAdapter {
|
||||
pub fn resolve(&self, source: &SourceRef) -> Result<AdapterResolution, DirectUrlAdapterError> {
|
||||
impl SourceAdapter for DirectUrlAdapter {
|
||||
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 {
|
||||
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 {
|
||||
|
|
@ -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::domain::source::{ResolvedRelease, SourceKind, SourceRef};
|
||||
|
||||
|
|
@ -14,24 +16,6 @@ impl GitHubAdapter {
|
|||
pub fn new() -> 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 {
|
||||
|
|
@ -45,9 +29,27 @@ impl SourceAdapter for GitHubAdapter {
|
|||
supports_exact_resolution: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum GitHubAdapterError {
|
||||
UnsupportedSource,
|
||||
fn normalize(&self, query: &str) -> Result<SourceRef, AdapterError> {
|
||||
let source = resolve_query(query).map_err(|_| AdapterError::UnsupportedQuery)?;
|
||||
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};
|
||||
|
||||
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 {
|
||||
fn id(&self) -> &'static str {
|
||||
"gitlab"
|
||||
|
|
@ -30,9 +17,27 @@ impl SourceAdapter for GitLabAdapter {
|
|||
supports_exact_resolution: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum GitLabAdapterError {
|
||||
UnsupportedSource,
|
||||
fn normalize(&self, query: &str) -> Result<SourceRef, AdapterError> {
|
||||
let source = resolve_query(query).map_err(|_| AdapterError::UnsupportedQuery)?;
|
||||
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;
|
||||
|
||||
|
|
@ -13,4 +16,12 @@ impl SourceAdapter for SourceForgeAdapter {
|
|||
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::SourceAdapter;
|
||||
use crate::adapters::traits::{
|
||||
AdapterCapabilities, AdapterError, AdapterResolution, SourceAdapter,
|
||||
};
|
||||
use crate::domain::source::SourceRef;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MockAdapter {
|
||||
|
|
@ -24,4 +26,12 @@ impl SourceAdapter for MockAdapter {
|
|||
fn capabilities(&self) -> AdapterCapabilities {
|
||||
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,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum AdapterError {
|
||||
UnsupportedQuery,
|
||||
UnsupportedSource,
|
||||
ResolutionFailed(String),
|
||||
}
|
||||
|
||||
pub trait SourceAdapter {
|
||||
fn id(&self) -> &'static str;
|
||||
|
||||
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;
|
||||
|
||||
|
|
@ -10,4 +13,12 @@ impl SourceAdapter for ZsyncAdapter {
|
|||
fn capabilities(&self) -> AdapterCapabilities {
|
||||
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 crate::domain::update::{ChannelPreference, PlannedUpdate, UpdateChannelKind, UpdatePlan};
|
||||
use std::path::Path;
|
||||
|
||||
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> {
|
||||
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)]
|
||||
pub enum BuildUpdatePlanError {}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum ExecuteUpdatesError {}
|
||||
|
||||
fn plan_update(app: &AppRecord) -> PlannedUpdate {
|
||||
let (selected_channel, selection_reason) = if let Some(strategy) = &app.update_strategy {
|
||||
if strategy.preferred.locator.contains("fail") {
|
||||
|
|
@ -47,3 +103,31 @@ fn plan_update(app: &AppRecord) -> PlannedUpdate {
|
|||
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)]
|
||||
pub enum ParsedMetadataKind {
|
||||
Unknown,
|
||||
|
|
@ -98,3 +100,41 @@ pub struct PlannedUpdate {
|
|||
pub selected_channel: ChannelPreference,
|
||||
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::traits::AdapterCapabilities;
|
||||
use aim_core::adapters::gitlab::GitLabAdapter;
|
||||
use aim_core::adapters::traits::{AdapterCapabilities, SourceAdapter};
|
||||
|
||||
#[test]
|
||||
fn adapter_capabilities_can_report_exact_resolution_only() {
|
||||
|
|
@ -9,10 +10,27 @@ fn adapter_capabilities_can_report_exact_resolution_only() {
|
|||
|
||||
#[test]
|
||||
fn legacy_github_adapter_delegates_to_source_pipeline() {
|
||||
let adapter = GitHubAdapter;
|
||||
let adapter: &dyn SourceAdapter = &GitHubAdapter;
|
||||
|
||||
let result = adapter.normalize("sharkdp/bat").unwrap();
|
||||
|
||||
assert_eq!(result.normalized_kind.as_str(), "github-repository");
|
||||
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::domain::app::AppRecord;
|
||||
use aim_core::app::update::{build_update_plan, execute_updates};
|
||||
use aim_core::domain::app::{AppRecord, InstallMetadata, InstallScope};
|
||||
use aim_core::domain::update::{ChannelPreference, UpdateChannelKind, UpdateStrategy};
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
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");
|
||||
}
|
||||
|
||||
#[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