Skip to content

Commit 21070db

Browse files
authored
feat(crate): init soar-registry crate (#119)
1 parent 135af26 commit 21070db

File tree

21 files changed

+1061
-548
lines changed

21 files changed

+1061
-548
lines changed

Cargo.lock

Lines changed: 19 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ members = [
44
"crates/soar-config",
55
"crates/soar-db",
66
"crates/soar-dl",
7+
"crates/soar-registry",
78
"crates/soar-utils",
89
"soar-cli",
910
"soar-core"
@@ -43,6 +44,7 @@ serial_test = "3.2.0"
4344
soar-config = { path = "crates/soar-config" }
4445
soar-core = { path = "soar-core" }
4546
soar-dl = { path = "crates/soar-dl" }
47+
soar-registry = { path = "crates/soar-registry" }
4648
soar-utils = { path = "crates/soar-utils" }
4749
tempfile = "3.10.1"
4850
thiserror = "2.0.17"
@@ -52,6 +54,7 @@ tracing = { version = "0.1.41", default-features = false }
5254
ureq = { version = "3.1.2", features = ["json"] }
5355
url = "2.5.7"
5456
xattr = "1.6.1"
57+
zstd = "0.13.3"
5558

5659
[profile.release]
5760
opt-level = 3

crates/soar-db/src/migration.rs

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,25 @@ pub enum DbType {
1313
Nest,
1414
}
1515

16+
fn get_migrations(db_type: &DbType) -> EmbeddedMigrations {
17+
match db_type {
18+
DbType::Core => CORE_MIGRATIONS,
19+
DbType::Metadata => METADATA_MIGRATIONS,
20+
DbType::Nest => NEST_MIGRATIONS,
21+
}
22+
}
23+
1624
pub fn apply_migrations(
1725
conn: &mut SqliteConnection,
1826
db_type: DbType,
1927
) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
2028
loop {
21-
let source = match db_type {
22-
DbType::Core => CORE_MIGRATIONS,
23-
DbType::Metadata => METADATA_MIGRATIONS,
24-
DbType::Nest => NEST_MIGRATIONS,
25-
};
26-
match conn.run_pending_migrations(source) {
29+
match conn.run_pending_migrations(get_migrations(&db_type)) {
2730
Ok(_) => break,
2831
Err(e) if e.to_string().contains("already exists") => {
29-
mark_first_pending(conn)?;
32+
mark_first_pending(conn, &db_type)?;
3033
}
31-
Err(e) => return Err(e.into()),
34+
Err(e) => return Err(e),
3235
}
3336
}
3437

@@ -37,8 +40,9 @@ pub fn apply_migrations(
3740

3841
fn mark_first_pending(
3942
conn: &mut SqliteConnection,
43+
db_type: &DbType,
4044
) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
41-
let pending = conn.pending_migrations(CORE_MIGRATIONS)?;
45+
let pending = conn.pending_migrations(get_migrations(db_type))?;
4246
if let Some(first) = pending.first() {
4347
sql_query("INSERT INTO __diesel_schema_migrations (version) VALUES (?1)")
4448
.bind::<diesel::sql_types::Text, _>(first.name().version())

crates/soar-registry/Cargo.toml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[package]
2+
name = "soar-registry"
3+
version = "0.1.0"
4+
description = "Registry management for soar package manager"
5+
authors.workspace = true
6+
edition.workspace = true
7+
readme.workspace = true
8+
repository.workspace = true
9+
license.workspace = true
10+
keywords.workspace = true
11+
categories.workspace = true
12+
13+
[dependencies]
14+
miette = { workspace = true }
15+
serde = { workspace = true }
16+
serde_json = { workspace = true }
17+
soar-config = { workspace = true }
18+
soar-dl = { workspace = true }
19+
soar-utils = { workspace = true }
20+
thiserror = { workspace = true }
21+
tracing = { workspace = true }
22+
ureq = { workspace = true }
23+
url = { workspace = true }
24+
zstd = { workspace = true }

crates/soar-registry/src/error.rs

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
//! Error types for the registry crate.
2+
//!
3+
//! This module defines [`RegistryError`], the error type used throughout
4+
//! the crate, along with helper traits for error context.
5+
6+
use miette::Diagnostic;
7+
use thiserror::Error;
8+
9+
/// Errors that can occur during registry operations.
10+
///
11+
/// This enum covers all error conditions that can arise when fetching,
12+
/// processing, or storing package metadata.
13+
#[derive(Error, Diagnostic, Debug)]
14+
pub enum RegistryError {
15+
#[error("IO error while {action}: {source}")]
16+
#[diagnostic(code(soar_registry::io))]
17+
IoError {
18+
action: String,
19+
source: std::io::Error,
20+
},
21+
22+
#[error("System time error: {0}")]
23+
#[diagnostic(code(soar_registry::system_time))]
24+
SystemTimeError(#[from] std::time::SystemTimeError),
25+
26+
#[error("HTTP request error: {0:?}")]
27+
#[diagnostic(
28+
code(soar_registry::http),
29+
help("Check your network connection and the repository URL")
30+
)]
31+
UreqError(#[from] ureq::Error),
32+
33+
#[error("Download failed: {0}")]
34+
#[diagnostic(code(soar_registry::download))]
35+
DownloadError(#[from] soar_dl::error::DownloadError),
36+
37+
#[error("Failed to fetch from remote source: {0}")]
38+
#[diagnostic(
39+
code(soar_registry::fetch_remote),
40+
help("Verify the repository URL is correct and accessible")
41+
)]
42+
FailedToFetchRemote(String),
43+
44+
#[error("JSON parse error: {0}")]
45+
#[diagnostic(
46+
code(soar_registry::json),
47+
help("The metadata file may be corrupted or in an invalid format")
48+
)]
49+
JsonError(#[from] serde_json::Error),
50+
51+
#[error("Invalid URL: {0}")]
52+
#[diagnostic(
53+
code(soar_registry::invalid_url),
54+
help("Ensure the URL is valid and properly formatted")
55+
)]
56+
InvalidUrl(String),
57+
58+
#[error("Metadata content is too short")]
59+
#[diagnostic(
60+
code(soar_registry::metadata_too_short),
61+
help("The metadata file appears to be corrupted or incomplete")
62+
)]
63+
MetadataTooShort,
64+
65+
#[error("ETag not found in metadata response")]
66+
#[diagnostic(
67+
code(soar_registry::missing_etag),
68+
help("The server did not return an ETag header")
69+
)]
70+
MissingEtag,
71+
72+
#[error("{0}")]
73+
#[diagnostic(code(soar_registry::custom))]
74+
Custom(String),
75+
}
76+
77+
/// A specialized Result type for registry operations.
78+
pub type Result<T> = std::result::Result<T, RegistryError>;
79+
80+
/// Extension trait for adding context to I/O errors.
81+
///
82+
/// This trait provides a convenient way to convert `std::io::Result` into
83+
/// [`Result`] with descriptive context about what operation failed.
84+
pub trait ErrorContext<T> {
85+
/// Adds context to an error, describing what action was being performed.
86+
///
87+
/// # Arguments
88+
///
89+
/// * `context` - A closure that returns a description of the failed action
90+
fn with_context<C>(self, context: C) -> Result<T>
91+
where
92+
C: FnOnce() -> String;
93+
}
94+
95+
impl<T> ErrorContext<T> for std::io::Result<T> {
96+
fn with_context<C>(self, context: C) -> Result<T>
97+
where
98+
C: FnOnce() -> String,
99+
{
100+
self.map_err(|err| {
101+
RegistryError::IoError {
102+
action: context(),
103+
source: err,
104+
}
105+
})
106+
}
107+
}
108+
109+
#[cfg(test)]
110+
mod tests {
111+
use super::*;
112+
113+
#[test]
114+
fn test_error_display() {
115+
let err = RegistryError::MetadataTooShort;
116+
assert_eq!(err.to_string(), "Metadata content is too short");
117+
118+
let err = RegistryError::MissingEtag;
119+
assert_eq!(err.to_string(), "ETag not found in metadata response");
120+
121+
let err = RegistryError::InvalidUrl("bad-url".to_string());
122+
assert_eq!(err.to_string(), "Invalid URL: bad-url");
123+
}
124+
}

crates/soar-registry/src/lib.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
//! Registry management for the soar package manager.
2+
//!
3+
//! This crate provides functionality for fetching, processing, and managing
4+
//! package metadata from remote repositories and nests.
5+
//!
6+
//! # Overview
7+
//!
8+
//! The crate handles two main types of metadata sources:
9+
//! - **Repositories**: Standard package repositories containing package metadata
10+
//! - **Nests**: User-defined package collections (similar to PPAs or custom repos)
11+
//!
12+
//! Metadata can be provided in two formats:
13+
//! - SQLite databases (`.sdb` files, optionally zstd-compressed)
14+
//! - JSON files containing package arrays
15+
//!
16+
//! # Example
17+
//!
18+
//! ```no_run
19+
//! use soar_registry::{fetch_metadata, MetadataContent};
20+
//! use soar_config::repository::Repository;
21+
//!
22+
//! async fn sync_repo(repo: &Repository) -> soar_registry::Result<()> {
23+
//! if let Some((etag, content)) = fetch_metadata(repo, false).await? {
24+
//! match content {
25+
//! MetadataContent::SqliteDb(bytes) => {
26+
//! // Write SQLite database to disk
27+
//! }
28+
//! MetadataContent::Json(packages) => {
29+
//! // Process JSON packages into a database
30+
//! }
31+
//! }
32+
//! }
33+
//! Ok(())
34+
//! }
35+
//! ```
36+
37+
pub mod error;
38+
pub mod metadata;
39+
pub mod nest;
40+
pub mod package;
41+
42+
pub use error::{ErrorContext, RegistryError, Result};
43+
pub use metadata::{
44+
fetch_metadata, fetch_nest_metadata, fetch_public_key, process_metadata_content,
45+
write_metadata_db, MetadataContent, SQLITE_MAGIC_BYTES, ZST_MAGIC_BYTES,
46+
};
47+
pub use nest::Nest;
48+
pub use package::RemotePackage;

0 commit comments

Comments
 (0)