initial skeleton
This commit is contained in:
parent
dc79fa2448
commit
71f89dde9c
60 changed files with 3480 additions and 0 deletions
13
crates/aim-core/src/adapters/custom_json.rs
Normal file
13
crates/aim-core/src/adapters/custom_json.rs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
use crate::adapters::traits::{AdapterCapabilities, SourceAdapter};
|
||||
|
||||
pub struct CustomJsonAdapter;
|
||||
|
||||
impl SourceAdapter for CustomJsonAdapter {
|
||||
fn id(&self) -> &'static str {
|
||||
"custom-json"
|
||||
}
|
||||
|
||||
fn capabilities(&self) -> AdapterCapabilities {
|
||||
AdapterCapabilities::exact_resolution_only()
|
||||
}
|
||||
}
|
||||
37
crates/aim-core/src/adapters/direct_url.rs
Normal file
37
crates/aim-core/src/adapters/direct_url.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
use crate::adapters::traits::{AdapterCapabilities, AdapterResolution, SourceAdapter};
|
||||
use crate::domain::source::{ResolvedRelease, SourceKind, SourceRef};
|
||||
|
||||
pub struct DirectUrlAdapter;
|
||||
|
||||
impl DirectUrlAdapter {
|
||||
pub fn resolve(&self, source: &SourceRef) -> Result<AdapterResolution, DirectUrlAdapterError> {
|
||||
if source.kind != SourceKind::DirectUrl {
|
||||
return Err(DirectUrlAdapterError::UnsupportedSource);
|
||||
}
|
||||
|
||||
Ok(AdapterResolution {
|
||||
source: SourceRef {
|
||||
kind: SourceKind::DirectUrl,
|
||||
locator: source.locator.clone(),
|
||||
},
|
||||
release: ResolvedRelease {
|
||||
version: "unresolved".to_owned(),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
50
crates/aim-core/src/adapters/github.rs
Normal file
50
crates/aim-core/src/adapters/github.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
use crate::adapters::traits::{AdapterCapabilities, AdapterResolution, SourceAdapter};
|
||||
use crate::domain::source::{ResolvedRelease, SourceKind, SourceRef};
|
||||
|
||||
pub struct GitHubAdapter;
|
||||
|
||||
impl Default for GitHubAdapter {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
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: SourceRef {
|
||||
kind: SourceKind::GitHub,
|
||||
locator: source.locator.clone(),
|
||||
},
|
||||
release: ResolvedRelease {
|
||||
version: "latest".to_owned(),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl SourceAdapter for GitHubAdapter {
|
||||
fn id(&self) -> &'static str {
|
||||
"github"
|
||||
}
|
||||
|
||||
fn capabilities(&self) -> AdapterCapabilities {
|
||||
AdapterCapabilities {
|
||||
supports_search: true,
|
||||
supports_exact_resolution: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum GitHubAdapterError {
|
||||
UnsupportedSource,
|
||||
}
|
||||
40
crates/aim-core/src/adapters/gitlab.rs
Normal file
40
crates/aim-core/src/adapters/gitlab.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
use crate::adapters::traits::{AdapterCapabilities, AdapterResolution, SourceAdapter};
|
||||
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: SourceRef {
|
||||
kind: SourceKind::GitLab,
|
||||
locator: source.locator.clone(),
|
||||
},
|
||||
release: ResolvedRelease {
|
||||
version: "latest".to_owned(),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl SourceAdapter for GitLabAdapter {
|
||||
fn id(&self) -> &'static str {
|
||||
"gitlab"
|
||||
}
|
||||
|
||||
fn capabilities(&self) -> AdapterCapabilities {
|
||||
AdapterCapabilities {
|
||||
supports_search: true,
|
||||
supports_exact_resolution: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum GitLabAdapterError {
|
||||
UnsupportedSource,
|
||||
}
|
||||
19
crates/aim-core/src/adapters/mod.rs
Normal file
19
crates/aim-core/src/adapters/mod.rs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
pub mod custom_json;
|
||||
pub mod direct_url;
|
||||
pub mod github;
|
||||
pub mod gitlab;
|
||||
pub mod sourceforge;
|
||||
pub mod test_support;
|
||||
pub mod traits;
|
||||
pub mod zsync;
|
||||
|
||||
pub fn all_adapter_kinds() -> Vec<&'static str> {
|
||||
vec![
|
||||
"github",
|
||||
"gitlab",
|
||||
"direct-url",
|
||||
"zsync",
|
||||
"sourceforge",
|
||||
"custom-json",
|
||||
]
|
||||
}
|
||||
16
crates/aim-core/src/adapters/sourceforge.rs
Normal file
16
crates/aim-core/src/adapters/sourceforge.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
use crate::adapters::traits::{AdapterCapabilities, SourceAdapter};
|
||||
|
||||
pub struct SourceForgeAdapter;
|
||||
|
||||
impl SourceAdapter for SourceForgeAdapter {
|
||||
fn id(&self) -> &'static str {
|
||||
"sourceforge"
|
||||
}
|
||||
|
||||
fn capabilities(&self) -> AdapterCapabilities {
|
||||
AdapterCapabilities {
|
||||
supports_search: true,
|
||||
supports_exact_resolution: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
27
crates/aim-core/src/adapters/test_support.rs
Normal file
27
crates/aim-core/src/adapters/test_support.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
use crate::adapters::traits::AdapterCapabilities;
|
||||
use crate::adapters::traits::SourceAdapter;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MockAdapter {
|
||||
id: &'static str,
|
||||
capabilities: AdapterCapabilities,
|
||||
}
|
||||
|
||||
impl MockAdapter {
|
||||
pub fn exact_resolution_only() -> Self {
|
||||
Self {
|
||||
id: "mock",
|
||||
capabilities: AdapterCapabilities::exact_resolution_only(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SourceAdapter for MockAdapter {
|
||||
fn id(&self) -> &'static str {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn capabilities(&self) -> AdapterCapabilities {
|
||||
self.capabilities
|
||||
}
|
||||
}
|
||||
29
crates/aim-core/src/adapters/traits.rs
Normal file
29
crates/aim-core/src/adapters/traits.rs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
use crate::domain::source::ResolvedRelease;
|
||||
use crate::domain::source::SourceRef;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct AdapterCapabilities {
|
||||
pub supports_search: bool,
|
||||
pub supports_exact_resolution: bool,
|
||||
}
|
||||
|
||||
impl AdapterCapabilities {
|
||||
pub fn exact_resolution_only() -> Self {
|
||||
Self {
|
||||
supports_search: false,
|
||||
supports_exact_resolution: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub struct AdapterResolution {
|
||||
pub source: SourceRef,
|
||||
pub release: ResolvedRelease,
|
||||
}
|
||||
|
||||
pub trait SourceAdapter {
|
||||
fn id(&self) -> &'static str;
|
||||
|
||||
fn capabilities(&self) -> AdapterCapabilities;
|
||||
}
|
||||
13
crates/aim-core/src/adapters/zsync.rs
Normal file
13
crates/aim-core/src/adapters/zsync.rs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
use crate::adapters::traits::{AdapterCapabilities, SourceAdapter};
|
||||
|
||||
pub struct ZsyncAdapter;
|
||||
|
||||
impl SourceAdapter for ZsyncAdapter {
|
||||
fn id(&self) -> &'static str {
|
||||
"zsync"
|
||||
}
|
||||
|
||||
fn capabilities(&self) -> AdapterCapabilities {
|
||||
AdapterCapabilities::exact_resolution_only()
|
||||
}
|
||||
}
|
||||
36
crates/aim-core/src/app/add.rs
Normal file
36
crates/aim-core/src/app/add.rs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
use crate::adapters::github::{GitHubAdapter, GitHubAdapterError};
|
||||
use crate::adapters::traits::AdapterResolution;
|
||||
use crate::app::query::{ResolveQueryError, resolve_query};
|
||||
use crate::domain::source::{SourceKind, SourceRef};
|
||||
|
||||
pub fn build_add_plan(query: &str) -> Result<AddPlan, BuildAddPlanError> {
|
||||
let source = resolve_query(query).map_err(BuildAddPlanError::Query)?;
|
||||
|
||||
let resolution = match source.kind {
|
||||
SourceKind::GitHub => GitHubAdapter::new()
|
||||
.resolve(&source)
|
||||
.map_err(BuildAddPlanError::GitHub)?,
|
||||
_ => AdapterResolution {
|
||||
source: SourceRef {
|
||||
kind: source.kind,
|
||||
locator: source.locator.clone(),
|
||||
},
|
||||
release: crate::domain::source::ResolvedRelease {
|
||||
version: "unresolved".to_owned(),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Ok(AddPlan { resolution })
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub struct AddPlan {
|
||||
pub resolution: AdapterResolution,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum BuildAddPlanError {
|
||||
Query(ResolveQueryError),
|
||||
GitHub(GitHubAdapterError),
|
||||
}
|
||||
77
crates/aim-core/src/app/identity.rs
Normal file
77
crates/aim-core/src/app/identity.rs
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
use crate::domain::app::{AppIdentity, IdentityConfidence};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum IdentityFallback {
|
||||
DisallowRawUrl,
|
||||
AllowRawUrl,
|
||||
}
|
||||
|
||||
pub fn resolve_identity(
|
||||
explicit_name: Option<&str>,
|
||||
explicit_id: Option<&str>,
|
||||
source_url: Option<&str>,
|
||||
fallback: IdentityFallback,
|
||||
) -> Result<AppIdentity, ResolveIdentityError> {
|
||||
if let Some(explicit_id) = explicit_id.filter(|value| !value.trim().is_empty()) {
|
||||
let stable_id = normalize_identifier(explicit_id);
|
||||
let display_name = explicit_name
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
.unwrap_or_else(|| explicit_id.to_owned());
|
||||
|
||||
return Ok(AppIdentity {
|
||||
stable_id,
|
||||
display_name,
|
||||
confidence: IdentityConfidence::Confident,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(explicit_name) = explicit_name.filter(|value| !value.trim().is_empty()) {
|
||||
return Ok(AppIdentity {
|
||||
stable_id: normalize_identifier(explicit_name),
|
||||
display_name: explicit_name.to_owned(),
|
||||
confidence: IdentityConfidence::NeedsConfirmation,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(source_url) = source_url.filter(|value| !value.trim().is_empty())
|
||||
&& fallback == IdentityFallback::AllowRawUrl
|
||||
{
|
||||
return Ok(AppIdentity {
|
||||
stable_id: normalize_url_identifier(source_url),
|
||||
display_name: source_url.to_owned(),
|
||||
confidence: IdentityConfidence::RawUrlFallback,
|
||||
});
|
||||
}
|
||||
|
||||
Err(ResolveIdentityError::Unresolved)
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum ResolveIdentityError {
|
||||
Unresolved,
|
||||
}
|
||||
|
||||
fn normalize_identifier(value: &str) -> String {
|
||||
value
|
||||
.trim()
|
||||
.chars()
|
||||
.map(|ch| match ch {
|
||||
'A'..='Z' => ch.to_ascii_lowercase(),
|
||||
'a'..='z' | '0'..='9' | '.' | '-' => ch,
|
||||
_ => '-',
|
||||
})
|
||||
.collect::<String>()
|
||||
.trim_matches('-')
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
fn normalize_url_identifier(url: &str) -> String {
|
||||
let trimmed = url
|
||||
.trim()
|
||||
.trim_start_matches("https://")
|
||||
.trim_start_matches("http://")
|
||||
.trim_start_matches("file://");
|
||||
|
||||
format!("url-{}", normalize_identifier(trimmed))
|
||||
}
|
||||
4
crates/aim-core/src/app/interaction.rs
Normal file
4
crates/aim-core/src/app/interaction.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum InteractionRequest {
|
||||
SelectRegisteredApp { query: String, matches: Vec<String> },
|
||||
}
|
||||
16
crates/aim-core/src/app/list.rs
Normal file
16
crates/aim-core/src/app/list.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
use crate::domain::app::AppRecord;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub struct ListRow {
|
||||
pub stable_id: String,
|
||||
pub display_name: String,
|
||||
}
|
||||
|
||||
pub fn build_list_rows(apps: &[AppRecord]) -> Vec<ListRow> {
|
||||
apps.iter()
|
||||
.map(|app| ListRow {
|
||||
stable_id: app.stable_id.clone(),
|
||||
display_name: app.display_name.clone(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
8
crates/aim-core/src/app/mod.rs
Normal file
8
crates/aim-core/src/app/mod.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
pub mod add;
|
||||
pub mod identity;
|
||||
pub mod interaction;
|
||||
pub mod list;
|
||||
pub mod query;
|
||||
pub mod remove;
|
||||
pub mod scope;
|
||||
pub mod update;
|
||||
55
crates/aim-core/src/app/query.rs
Normal file
55
crates/aim-core/src/app/query.rs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
use crate::domain::source::SourceKind;
|
||||
use crate::domain::source::SourceRef;
|
||||
|
||||
pub fn resolve_query(query: &str) -> Result<SourceRef, ResolveQueryError> {
|
||||
if query.starts_with("file://") {
|
||||
return Ok(SourceRef {
|
||||
kind: SourceKind::File,
|
||||
locator: query.to_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
if query.starts_with("https://gitlab.com/") || query.starts_with("http://gitlab.com/") {
|
||||
return Ok(SourceRef {
|
||||
kind: SourceKind::GitLab,
|
||||
locator: query.to_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
if query.starts_with("https://") || query.starts_with("http://") {
|
||||
return Ok(SourceRef {
|
||||
kind: SourceKind::DirectUrl,
|
||||
locator: query.to_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
if is_github_shorthand(query) {
|
||||
return Ok(SourceRef {
|
||||
kind: SourceKind::GitHub,
|
||||
locator: query.to_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
Err(ResolveQueryError::Unsupported)
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum ResolveQueryError {
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
fn is_github_shorthand(query: &str) -> bool {
|
||||
let mut parts = query.split('/');
|
||||
let Some(owner) = parts.next() else {
|
||||
return false;
|
||||
};
|
||||
let Some(repo) = parts.next() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if parts.next().is_some() {
|
||||
return false;
|
||||
}
|
||||
|
||||
!owner.is_empty() && !repo.is_empty() && !owner.contains(':') && !repo.contains(':')
|
||||
}
|
||||
80
crates/aim-core/src/app/remove.rs
Normal file
80
crates/aim-core/src/app/remove.rs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
use crate::app::interaction::InteractionRequest;
|
||||
use crate::domain::app::AppRecord;
|
||||
|
||||
pub fn resolve_registered_app<'a>(
|
||||
query: &str,
|
||||
apps: &'a [AppRecord],
|
||||
) -> Result<&'a AppRecord, ResolveRegisteredAppError> {
|
||||
let normalized_query = normalize_lookup(query);
|
||||
let matches = apps
|
||||
.iter()
|
||||
.filter(|app| {
|
||||
normalize_lookup(&app.stable_id) == normalized_query
|
||||
|| normalize_lookup(&app.display_name) == normalized_query
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
match matches.as_slice() {
|
||||
[] => Err(ResolveRegisteredAppError::UnknownApp {
|
||||
query: query.to_owned(),
|
||||
}),
|
||||
[app] => Ok(*app),
|
||||
_ => Err(ResolveRegisteredAppError::Ambiguous {
|
||||
request: InteractionRequest::SelectRegisteredApp {
|
||||
query: query.to_owned(),
|
||||
matches: matches
|
||||
.iter()
|
||||
.map(|app| format!("{} ({})", app.display_name, app.stable_id))
|
||||
.collect(),
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub struct RemovalPlan {
|
||||
pub stable_id: String,
|
||||
pub display_name: String,
|
||||
pub artifact_paths: Vec<String>,
|
||||
}
|
||||
|
||||
pub fn build_removal_plan(app: &AppRecord) -> RemovalPlan {
|
||||
RemovalPlan {
|
||||
stable_id: app.stable_id.clone(),
|
||||
display_name: app.display_name.clone(),
|
||||
artifact_paths: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_registered_app(
|
||||
query: &str,
|
||||
apps: &[AppRecord],
|
||||
) -> Result<RemovalResult, ResolveRegisteredAppError> {
|
||||
let app = resolve_registered_app(query, apps)?;
|
||||
let remaining_apps = apps
|
||||
.iter()
|
||||
.filter(|candidate| candidate.stable_id != app.stable_id)
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
Ok(RemovalResult {
|
||||
removed: build_removal_plan(app),
|
||||
remaining_apps,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub struct RemovalResult {
|
||||
pub removed: RemovalPlan,
|
||||
pub remaining_apps: Vec<AppRecord>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum ResolveRegisteredAppError {
|
||||
UnknownApp { query: String },
|
||||
Ambiguous { request: InteractionRequest },
|
||||
}
|
||||
|
||||
fn normalize_lookup(value: &str) -> String {
|
||||
value.trim().to_ascii_lowercase()
|
||||
}
|
||||
28
crates/aim-core/src/app/scope.rs
Normal file
28
crates/aim-core/src/app/scope.rs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
use crate::domain::app::InstallScope;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum ScopeOverride {
|
||||
System,
|
||||
User,
|
||||
}
|
||||
|
||||
pub fn resolve_install_scope(
|
||||
_is_effective_root: bool,
|
||||
override_scope: ScopeOverride,
|
||||
) -> InstallScope {
|
||||
match override_scope {
|
||||
ScopeOverride::System => InstallScope::System,
|
||||
ScopeOverride::User => InstallScope::User,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_install_scope_with_default(
|
||||
is_effective_root: bool,
|
||||
override_scope: Option<ScopeOverride>,
|
||||
) -> InstallScope {
|
||||
match override_scope {
|
||||
Some(scope) => resolve_install_scope(is_effective_root, scope),
|
||||
None if is_effective_root => InstallScope::System,
|
||||
None => InstallScope::User,
|
||||
}
|
||||
}
|
||||
17
crates/aim-core/src/app/update.rs
Normal file
17
crates/aim-core/src/app/update.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
use crate::domain::app::AppRecord;
|
||||
use crate::domain::update::{PlannedUpdate, UpdatePlan};
|
||||
|
||||
pub fn build_update_plan(apps: &[AppRecord]) -> Result<UpdatePlan, BuildUpdatePlanError> {
|
||||
Ok(UpdatePlan {
|
||||
items: apps
|
||||
.iter()
|
||||
.map(|app| PlannedUpdate {
|
||||
stable_id: app.stable_id.clone(),
|
||||
display_name: app.display_name.clone(),
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum BuildUpdatePlanError {}
|
||||
25
crates/aim-core/src/domain/app.rs
Normal file
25
crates/aim-core/src/domain/app.rs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum InstallScope {
|
||||
User,
|
||||
System,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum IdentityConfidence {
|
||||
Confident,
|
||||
NeedsConfirmation,
|
||||
RawUrlFallback,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub struct AppIdentity {
|
||||
pub stable_id: String,
|
||||
pub display_name: String,
|
||||
pub confidence: IdentityConfidence,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct AppRecord {
|
||||
pub stable_id: String,
|
||||
pub display_name: String,
|
||||
}
|
||||
3
crates/aim-core/src/domain/mod.rs
Normal file
3
crates/aim-core/src/domain/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
pub mod app;
|
||||
pub mod source;
|
||||
pub mod update;
|
||||
29
crates/aim-core/src/domain/source.rs
Normal file
29
crates/aim-core/src/domain/source.rs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum SourceKind {
|
||||
GitHub,
|
||||
GitLab,
|
||||
DirectUrl,
|
||||
File,
|
||||
}
|
||||
|
||||
impl SourceKind {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::GitHub => "github",
|
||||
Self::GitLab => "gitlab",
|
||||
Self::DirectUrl => "direct-url",
|
||||
Self::File => "file",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub struct SourceRef {
|
||||
pub kind: SourceKind,
|
||||
pub locator: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub struct ResolvedRelease {
|
||||
pub version: String,
|
||||
}
|
||||
10
crates/aim-core/src/domain/update.rs
Normal file
10
crates/aim-core/src/domain/update.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub struct UpdatePlan {
|
||||
pub items: Vec<PlannedUpdate>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub struct PlannedUpdate {
|
||||
pub stable_id: String,
|
||||
pub display_name: String,
|
||||
}
|
||||
14
crates/aim-core/src/integration/install.rs
Normal file
14
crates/aim-core/src/integration/install.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub fn staged_appimage_path(staging_root: &Path, app_id: &str) -> PathBuf {
|
||||
staging_root.join(format!("{app_id}.download"))
|
||||
}
|
||||
|
||||
pub fn replacement_path(target: &Path) -> PathBuf {
|
||||
let mut file_name = target
|
||||
.file_name()
|
||||
.map(|name| name.to_os_string())
|
||||
.unwrap_or_default();
|
||||
file_name.push(".new");
|
||||
target.with_file_name(file_name)
|
||||
}
|
||||
2
crates/aim-core/src/integration/mod.rs
Normal file
2
crates/aim-core/src/integration/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pub mod install;
|
||||
pub mod paths;
|
||||
40
crates/aim-core/src/integration/paths.rs
Normal file
40
crates/aim-core/src/integration/paths.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::domain::app::InstallScope;
|
||||
use crate::platform::{
|
||||
system_applications_dir, system_icons_dir, system_managed_appimages_dir, user_applications_dir,
|
||||
user_icons_dir, user_managed_appimages_dir,
|
||||
};
|
||||
|
||||
pub fn managed_appimage_path(home_dir: &Path, scope: InstallScope, app_id: &str) -> PathBuf {
|
||||
scope_managed_dir(home_dir, scope).join(format!("{app_id}.AppImage"))
|
||||
}
|
||||
|
||||
pub fn desktop_entry_path(home_dir: &Path, scope: InstallScope, app_id: &str) -> PathBuf {
|
||||
scope_applications_dir(home_dir, scope).join(format!("aim-{app_id}.desktop"))
|
||||
}
|
||||
|
||||
pub fn icon_path(home_dir: &Path, scope: InstallScope, app_id: &str) -> PathBuf {
|
||||
scope_icons_dir(home_dir, scope).join(format!("{app_id}.png"))
|
||||
}
|
||||
|
||||
fn scope_managed_dir(home_dir: &Path, scope: InstallScope) -> PathBuf {
|
||||
match scope {
|
||||
InstallScope::User => user_managed_appimages_dir(home_dir),
|
||||
InstallScope::System => system_managed_appimages_dir(),
|
||||
}
|
||||
}
|
||||
|
||||
fn scope_applications_dir(home_dir: &Path, scope: InstallScope) -> PathBuf {
|
||||
match scope {
|
||||
InstallScope::User => user_applications_dir(home_dir),
|
||||
InstallScope::System => system_applications_dir(),
|
||||
}
|
||||
}
|
||||
|
||||
fn scope_icons_dir(home_dir: &Path, scope: InstallScope) -> PathBuf {
|
||||
match scope {
|
||||
InstallScope::User => user_icons_dir(home_dir),
|
||||
InstallScope::System => system_icons_dir(),
|
||||
}
|
||||
}
|
||||
6
crates/aim-core/src/lib.rs
Normal file
6
crates/aim-core/src/lib.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
pub mod adapters;
|
||||
pub mod app;
|
||||
pub mod domain;
|
||||
pub mod integration;
|
||||
pub mod platform;
|
||||
pub mod registry;
|
||||
25
crates/aim-core/src/platform/mod.rs
Normal file
25
crates/aim-core/src/platform/mod.rs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub fn user_managed_appimages_dir(home_dir: &Path) -> PathBuf {
|
||||
home_dir.join(".local/lib/aim/appimages")
|
||||
}
|
||||
|
||||
pub fn user_applications_dir(home_dir: &Path) -> PathBuf {
|
||||
home_dir.join(".local/share/applications")
|
||||
}
|
||||
|
||||
pub fn user_icons_dir(home_dir: &Path) -> PathBuf {
|
||||
home_dir.join(".local/share/icons/hicolor/256x256/apps")
|
||||
}
|
||||
|
||||
pub fn system_managed_appimages_dir() -> PathBuf {
|
||||
PathBuf::from("/opt/aim/appimages")
|
||||
}
|
||||
|
||||
pub fn system_applications_dir() -> PathBuf {
|
||||
PathBuf::from("/usr/share/applications")
|
||||
}
|
||||
|
||||
pub fn system_icons_dir() -> PathBuf {
|
||||
PathBuf::from("/usr/share/icons/hicolor/256x256/apps")
|
||||
}
|
||||
2
crates/aim-core/src/registry/mod.rs
Normal file
2
crates/aim-core/src/registry/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pub mod model;
|
||||
pub mod store;
|
||||
14
crates/aim-core/src/registry/model.rs
Normal file
14
crates/aim-core/src/registry/model.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Registry {
|
||||
pub version: u32,
|
||||
pub apps: Vec<crate::domain::app::AppRecord>,
|
||||
}
|
||||
|
||||
impl Default for Registry {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
version: 1,
|
||||
apps: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
59
crates/aim-core/src/registry/store.rs
Normal file
59
crates/aim-core/src/registry/store.rs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::registry::model::Registry;
|
||||
|
||||
pub struct RegistryStore {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl RegistryStore {
|
||||
pub fn new(path: PathBuf) -> Self {
|
||||
Self { path }
|
||||
}
|
||||
|
||||
pub fn load(&self) -> Result<Registry, RegistryStoreError> {
|
||||
if !self.path.exists() {
|
||||
return Ok(Registry::default());
|
||||
}
|
||||
|
||||
let contents = fs::read_to_string(&self.path)?;
|
||||
let registry = toml::from_str(&contents)?;
|
||||
Ok(registry)
|
||||
}
|
||||
|
||||
pub fn save(&self, registry: &Registry) -> Result<(), RegistryStoreError> {
|
||||
if let Some(parent) = self.path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
let contents = toml::to_string(registry)?;
|
||||
fs::write(&self.path, contents)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum RegistryStoreError {
|
||||
Io(std::io::Error),
|
||||
SerializeToml(toml::ser::Error),
|
||||
Toml(toml::de::Error),
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for RegistryStoreError {
|
||||
fn from(error: std::io::Error) -> Self {
|
||||
Self::Io(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<toml::de::Error> for RegistryStoreError {
|
||||
fn from(error: toml::de::Error) -> Self {
|
||||
Self::Toml(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<toml::ser::Error> for RegistryStoreError {
|
||||
fn from(error: toml::ser::Error) -> Self {
|
||||
Self::SerializeToml(error)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue