Radish alpha
r
rad:zwTxygwuz5LDGBq255RA2CbNGrz8
Radicle CI broker
Radicle
Git
refactor: avoid exposing types from dependencies in error types
Lars Wirzenius committed 4 months ago
commit e90dfebddec256775492fa779dfb6b9f9f9e31c5
parent 1921aed
16 files changed +429 -237
modified src/adapter.rs
@@ -75,7 +75,7 @@ impl Adapters {
    }

    pub fn to_json(&self) -> Result<String, AdapterError> {
-
        serde_json::to_string_pretty(self).map_err(AdapterError::AdaptersToJson)
+
        serde_json::to_string_pretty(self).map_err(AdapterError::adapters_to_json)
    }
}

@@ -125,7 +125,7 @@ impl Adapter {
        let filename = tmpdir.path().join("adapter.yaml");

        let yaml =
-
            serde_norway::to_string(&self.config).map_err(AdapterError::AdapterConfigToYaml)?;
+
            serde_norway::to_string(&self.config).map_err(AdapterError::adapter_config_to_yaml)?;
        logger::adapter_temp_config(&filename, &yaml);
        std::fs::write(&filename, yaml.as_bytes()).map_err(AdapterError::AdapterConfigWrite)?;

@@ -201,9 +201,7 @@ impl Adapter {
        child.feed_stdin(trigger.to_string().as_bytes());
        let child = match child.spawn(cmd) {
            Ok(child) => child,
-
            Err(TimeoutError::Spawn(_, err)) => {
-
                Err(AdapterError::SpawnAdapter(self.bin.clone(), err))?
-
            }
+
            Err(TimeoutError::Spawn(_, err)) => Err(AdapterError::spawn_adapter(&self.bin, err))?,
            Err(err) => Err(AdapterError::TimeoutCommand(err))?,
        };
        let child_info = ChildInfo::new(run.broker_run_id().clone(), child.id());
@@ -375,7 +373,7 @@ pub enum AdapterError {

    /// Can't serialize [`Adapters`] to JSON.
    #[error("failed to serialize adapters as JSON")]
-
    AdaptersToJson(#[source] serde_json::Error),
+
    AdaptersToJson(#[source] Box<dyn std::error::Error>),

    /// Error from [`TimeoutCommand`] or [`RunningProcess`].
    #[error(transparent)]
@@ -466,7 +464,7 @@ pub enum AdapterError {

    /// Can't serialize adapter configuration to YAML.
    #[error("can't convert adapter configuration to YAML")]
-
    AdapterConfigToYaml(#[source] serde_norway::Error),
+
    AdapterConfigToYaml(#[source] Box<dyn std::error::Error>),

    /// Can't write adapter config.
    #[error("failed to write adapter configuration")]
@@ -477,6 +475,20 @@ pub enum AdapterError {
    Mutex,
}

+
impl AdapterError {
+
    fn adapters_to_json(err: serde_json::Error) -> Self {
+
        Self::AdaptersToJson(Box::new(err))
+
    }
+

+
    fn spawn_adapter(path: &Path, err: std::io::Error) -> Self {
+
        Self::SpawnAdapter(path.into(), err)
+
    }
+

+
    fn adapter_config_to_yaml(err: serde_norway::Error) -> Self {
+
        Self::AdapterConfigToYaml(Box::new(err))
+
    }
+
}
+

#[cfg(test)]
mod test {
    use super::*;
modified src/broker.rs
@@ -4,7 +4,7 @@
//! testing.

use std::{
-
    path::{Path, PathBuf},
+
    path::Path,
    sync::{Arc, Mutex, mpsc::Sender},
    time::Duration,
};
@@ -131,7 +131,7 @@ impl Broker {
            .repo_id(common.repository.id)
            .repo_name(&common.repository.name)
            .whence(whence)
-
            .timestamp(now()?)
+
            .timestamp(now().map_err(BrokerError::time_format)?)
            .build();
        self.db.push_run(&run)?;

@@ -178,15 +178,7 @@ fn now() -> Result<String, time::error::Format> {
pub enum BrokerError {
    /// Error formatting a time as a string.
    #[error(transparent)]
-
    Timeformat(#[from] time::error::Format),
-

-
    /// Error from Radicle.
-
    #[error(transparent)]
-
    RadicleProfile(#[from] radicle::profile::Error),
-

-
    /// Error from spawning a sub-process.
-
    #[error("failed to spawn a CI adapter sub-process: {0}")]
-
    SpawnAdapter(PathBuf, #[source] std::io::Error),
+
    Timeformat(#[from] Box<dyn std::error::Error + Send + 'static>),

    /// Default adapter is not in list of adapters.
    #[error("default adapter is not in list of adapters")]
@@ -196,14 +188,6 @@ pub enum BrokerError {
    #[error("could not determine what adapter to use for repository {0}")]
    NoAdapter(RepoId),

-
    /// Request is not a trigger message.
-
    #[error("tried to execute CI based on a message that is not a trigger one: {0:#?}")]
-
    NotTrigger(Box<Request>),
-

-
    /// Could not convert repository ID from string.
-
    #[error("failed to understand repository id {0:?}")]
-
    BadRepoId(String, #[source] radicle::identity::IdError),
-

    /// Patch event doesn't have any revisions.
    #[error("expected at least one revision in a patch event")]
    NoRevisions,
@@ -213,6 +197,12 @@ pub enum BrokerError {
    Db(#[from] DbError),
}

+
impl BrokerError {
+
    fn time_format(err: time::error::Format) -> Self {
+
        Self::Timeformat(Box::new(err))
+
    }
+
}
+

#[cfg(test)]
mod test {
    use super::*;
modified src/ci_event.rs
@@ -349,7 +349,7 @@ impl CiEvent {
    }

    pub fn to_pretty_json(&self) -> Result<String, CiEventError> {
-
        serde_json::to_string_pretty(self).map_err(CiEventError::ToJson)
+
        serde_json::to_string_pretty(self).map_err(CiEventError::to_json)
    }
}

@@ -391,13 +391,19 @@ pub enum CiEventError {
    NotUtf8(PathBuf, #[source] std::string::FromUtf8Error),

    #[error("broker events file is not valid JSON: {0}")]
-
    NotJson(PathBuf, #[source] serde_json::Error),
+
    NotJson(
+
        PathBuf,
+
        #[source] Box<dyn std::error::Error + Send + 'static>,
+
    ),

    #[error("failed to convert name spaced Git ref into node public key: {0}")]
-
    KeyFromNamespaced(RefString, #[source] radicle_crypto::PublicKeyError),
+
    KeyFromNamespaced(
+
        RefString,
+
        #[source] Box<dyn std::error::Error + Send + 'static>,
+
    ),

    #[error("failed to encode CI event as JSON")]
-
    ToJson(#[source] serde_json::Error),
+
    ToJson(#[source] Box<dyn std::error::Error + Send + 'static>),
}

impl CiEventError {
@@ -410,11 +416,15 @@ impl CiEventError {
    }

    fn not_json(filename: &Path, err: serde_json::Error) -> Self {
-
        Self::NotJson(filename.into(), err)
+
        Self::NotJson(filename.into(), Box::new(err))
+
    }
+

+
    fn to_json(err: serde_json::Error) -> Self {
+
        Self::ToJson(Box::new(err))
    }

    fn key_from_namespaced(name: &Namespaced, err: radicle_crypto::PublicKeyError) -> Self {
-
        Self::KeyFromNamespaced(name.to_ref_string(), err)
+
        Self::KeyFromNamespaced(name.to_ref_string(), Box::new(err))
    }
}

modified src/ci_event_source.rs
@@ -30,7 +30,7 @@ impl CiEventSource {
            }
            Err(err) => {
                logger::error("error reading event from node", &err);
-
                if let NodeEventError::Node(radicle::node::Error::InvalidJson { .. }) = err {
+
                if let NodeEventError::Node(_) = err {
                    Ok(Some(Vec::new()))
                } else {
                    Err(CiEventSourceError::NodeEventError(err))
modified src/cob.rs
@@ -38,7 +38,7 @@ pub struct KnownJobCobs {
impl KnownJobCobs {
    /// Create a new [`KnownJobCobs`].
    pub fn new() -> Result<Self, JobError> {
-
        let profile = Profile::load().map_err(JobError::Profile)?;
+
        let profile = Profile::load().map_err(JobError::profile)?;
        Ok(Self {
            profile,
            known: HashMap::new(),
@@ -60,12 +60,12 @@ impl KnownJobCobs {

    fn fallible_create_job(&self, repo_id: RepoId, oid: Oid) -> Result<JobId, JobError> {
        let repo = repository(&self.profile, repo_id)?;
-
        let signer = self.profile.signer().map_err(JobError::Signer)?;
+
        let signer = self.profile.signer().map_err(JobError::signer)?;

        let mut jobs = jobs(&repo)?;
        match job_for_commit(&jobs, oid) {
            Err(JobError::NoJob(_)) => {
-
                let job = jobs.create(oid, &signer).map_err(JobError::CreateJob)?;
+
                let job = jobs.create(oid, &signer).map_err(JobError::create_job)?;
                announce(&self.profile, repo_id, false)?;
                logger::job_create(&repo_id, &oid, job.id());
                Ok(*job.id())
@@ -117,15 +117,15 @@ impl KnownJobCobs {
        url: &Url,
        a: bool,
    ) -> Result<(), JobError> {
-
        let uuid = Uuid::from_str(run_id.as_str()).map_err(JobError::Uuid)?;
+
        let uuid = Uuid::from_str(run_id.as_str()).map_err(JobError::uuid)?;

        let repo = repository(&self.profile, repo_id)?;
-
        let signer = self.profile.signer().map_err(JobError::Signer)?;
+
        let signer = self.profile.signer().map_err(JobError::signer)?;

        let mut jobs = jobs(&repo)?;
-
        let mut job = jobs.get_mut(&job_id).map_err(JobError::GetJobMut)?;
+
        let mut job = jobs.get_mut(&job_id).map_err(JobError::get_jobs_mut)?;
        job.run(uuid, url.clone(), &signer)
-
            .map_err(JobError::AddRun)?;
+
            .map_err(JobError::add_run)?;

        announce(&self.profile, repo_id, a)?;

@@ -154,17 +154,17 @@ pub fn failed(repo_id: RepoId, oid: Oid, run_id: RunId) {
}

fn finish(repo_id: RepoId, oid: Oid, run_id: RunId, reason: Reason) -> Result<(), JobError> {
-
    let uuid = Uuid::from_str(run_id.as_str()).map_err(JobError::Uuid)?;
+
    let uuid = Uuid::from_str(run_id.as_str()).map_err(JobError::uuid)?;

    let profile = profile()?;
    let repo = repository(&profile, repo_id)?;
-
    let signer = profile.signer().map_err(JobError::Signer)?;
+
    let signer = profile.signer().map_err(JobError::signer)?;

    let mut jobs = jobs(&repo)?;
    let job_id = job_for_commit(&jobs, oid)?;
-
    let mut job = jobs.get_mut(&job_id).map_err(JobError::GetJobMut)?;
+
    let mut job = jobs.get_mut(&job_id).map_err(JobError::get_jobs_mut)?;
    job.finish(uuid, reason, &signer)
-
        .map_err(JobError::Finish)?;
+
        .map_err(JobError::finish)?;
    announce(&profile, repo_id, true)?;

    logger::job_run_finished(job_id, uuid, reason);
@@ -172,23 +172,23 @@ fn finish(repo_id: RepoId, oid: Oid, run_id: RunId, reason: Reason) -> Result<()
}

fn profile() -> Result<Profile, JobError> {
-
    Profile::load().map_err(JobError::Profile)
+
    Profile::load().map_err(JobError::profile)
}

fn repository(profile: &Profile, repo_id: RepoId) -> Result<Repository, JobError> {
    profile
        .storage
        .repository(repo_id)
-
        .map_err(JobError::OpenRepository)
+
        .map_err(JobError::open_repository)
}

fn jobs<'a>(repo: &'a Repository) -> Result<Jobs<'a, Repository>, JobError> {
-
    Jobs::open(repo).map_err(JobError::Jobs)
+
    Jobs::open(repo).map_err(JobError::jobs)
}

fn job_for_commit<'a>(jobs: &Jobs<'a, Repository>, wanted: Oid) -> Result<JobId, JobError> {
-
    for item in jobs.all().map_err(JobError::AllJobs)? {
-
        let (job_id, job) = item.map_err(JobError::AllJobsJob)?;
+
    for item in jobs.all().map_err(JobError::all_jobs)? {
+
        let (job_id, job) = item.map_err(JobError::all_jobs_job)?;
        let job_id = JobId::from(job_id);
        if job.oid() == &wanted {
            return Ok(job_id);
@@ -204,7 +204,7 @@ fn announce(profile: &Profile, repo_id: RepoId, announce: bool) -> Result<(), Jo

        let mut node = Node::new(profile.home.socket());

-
        let (synced, unsynced) = node.seeds(repo_id).map_err(JobError::Seeds)?.iter().fold(
+
        let (synced, unsynced) = node.seeds(repo_id).map_err(JobError::seeds)?.iter().fold(
            (BTreeSet::new(), BTreeSet::new()),
            |(mut synced, mut unsynced), seed| {
                if seed.is_synced() {
@@ -225,7 +225,7 @@ fn announce(profile: &Profile, repo_id: RepoId, announce: bool) -> Result<(), Jo
        ))
        .map_err(|_| JobError::Announcer)?;
        node.announce(repo_id, TIMEOUT, announcer, |_, _| ())
-
            .map_err(JobError::Announce)?;
+
            .map_err(JobError::announce)?;
    }

    Ok(())
@@ -235,37 +235,37 @@ fn announce(profile: &Profile, repo_id: RepoId, announce: bool) -> Result<(), Jo
#[derive(Debug, thiserror::Error)]
pub enum JobError {
    #[error("failed to load Radicle profile")]
-
    Profile(#[source] radicle::profile::Error),
+
    Profile(#[source] Box<dyn std::error::Error + Send + 'static>),

    #[error("failed to open repository in Radicle node storage")]
-
    OpenRepository(#[source] radicle::storage::RepositoryError),
+
    OpenRepository(#[source] Box<dyn std::error::Error + Send + 'static>),

    #[error("failed to list job COBs in repository")]
-
    Jobs(#[source] radicle::storage::RepositoryError),
+
    Jobs(#[source] Box<dyn std::error::Error + Send + 'static>),

    #[error("failed to get all job COBs")]
-
    AllJobs(#[source] radicle::cob::store::Error),
+
    AllJobs(#[source] Box<dyn std::error::Error + Send + 'static>),

    #[error("failed to create a new job COB")]
-
    CreateJob(#[source] radicle::cob::store::Error),
+
    CreateJob(#[source] Box<dyn std::error::Error + Send + 'static>),

    #[error("failed to create a signer for Radicle repository")]
-
    Signer(#[source] radicle::profile::Error),
+
    Signer(#[source] Box<dyn std::error::Error + Send + 'static>),

    #[error("couldn't get job when iterating")]
-
    AllJobsJob(#[source] radicle::cob::store::Error),
+
    AllJobsJob(#[source] Box<dyn std::error::Error + Send + 'static>),

    #[error("failed to get mutable job COB")]
-
    GetJobMut(#[source] radicle::cob::store::Error),
+
    GetJobMut(#[source] Box<dyn std::error::Error + Send + 'static>),

    #[error("failed to add a run to a job COB")]
-
    AddRun(#[source] radicle::cob::store::Error),
+
    AddRun(#[source] Box<dyn std::error::Error + Send + 'static>),

    #[error("could not mark a run as finished")]
-
    Finish(#[source] radicle::cob::store::Error),
+
    Finish(#[source] Box<dyn std::error::Error + Send + 'static>),

    #[error("failed to construct a UUID from a run id")]
-
    Uuid(#[source] uuid::Error),
+
    Uuid(#[source] Box<dyn std::error::Error + Send + 'static>),

    #[error("failed to find job COB for oid {0}")]
    NoJob(Oid),
@@ -274,11 +274,65 @@ pub enum JobError {
    JobExists(Oid),

    #[error("failed to get seeds for node")]
-
    Seeds(#[source] radicle::node::Error),
+
    Seeds(#[source] Box<dyn std::error::Error + Send + 'static>),

    #[error("failed to announce COB change")]
-
    Announce(#[source] radicle::node::Error),
+
    Announce(#[source] Box<dyn std::error::Error + Send + 'static>),

    #[error("failed to create a COB announcer")]
    Announcer,
}
+

+
impl JobError {
+
    fn profile(err: radicle::profile::Error) -> Self {
+
        Self::Profile(Box::new(err))
+
    }
+

+
    fn open_repository(err: radicle::storage::RepositoryError) -> Self {
+
        Self::OpenRepository(Box::new(err))
+
    }
+

+
    fn jobs(err: radicle::storage::RepositoryError) -> Self {
+
        Self::Jobs(Box::new(err))
+
    }
+

+
    fn all_jobs(err: radicle::cob::store::Error) -> Self {
+
        Self::AllJobs(Box::new(err))
+
    }
+

+
    fn all_jobs_job(err: radicle::cob::store::Error) -> Self {
+
        Self::AllJobsJob(Box::new(err))
+
    }
+

+
    fn get_jobs_mut(err: radicle::cob::store::Error) -> Self {
+
        Self::GetJobMut(Box::new(err))
+
    }
+

+
    fn add_run(err: radicle::cob::store::Error) -> Self {
+
        Self::AddRun(Box::new(err))
+
    }
+

+
    fn finish(err: radicle::cob::store::Error) -> Self {
+
        Self::Finish(Box::new(err))
+
    }
+

+
    fn create_job(err: radicle::cob::store::Error) -> Self {
+
        Self::CreateJob(Box::new(err))
+
    }
+

+
    fn signer(err: radicle::profile::Error) -> Self {
+
        Self::Signer(Box::new(err))
+
    }
+

+
    fn uuid(err: uuid::Error) -> Self {
+
        Self::Uuid(Box::new(err))
+
    }
+

+
    fn seeds(err: radicle::node::Error) -> Self {
+
        Self::Seeds(Box::new(err))
+
    }
+

+
    fn announce(err: radicle::node::Error) -> Self {
+
        Self::Announce(Box::new(err))
+
    }
+
}
modified src/config.rs
@@ -66,13 +66,12 @@ fn default_queue_len_interval() -> Duration {

impl Config {
    pub fn load(filename: &Path) -> Result<Self, ConfigError> {
-
        let config =
-
            std::fs::read(filename).map_err(|e| ConfigError::ReadConfig(filename.into(), e))?;
+
        let config = std::fs::read(filename).map_err(|e| ConfigError::read_config(filename, e))?;
        let config: Config = match filename.extension().and_then(OsStr::to_str) {
            Some("json") => serde_json::from_slice(&config)
-
                .map_err(|e| ConfigError::ParseConfigJson(filename.into(), e))?,
+
                .map_err(|e| ConfigError::parse_config_json(filename, e))?,
            _ => serde_norway::from_slice(&config)
-
                .map_err(|e| ConfigError::ParseConfig(filename.into(), e))?,
+
                .map_err(|e| ConfigError::parse_config(filename, e))?,
        };
        config.check(filename)?;
        Ok(config)
@@ -142,7 +141,7 @@ impl Config {
    }

    pub fn to_json(&self) -> Result<String, ConfigError> {
-
        serde_json::to_string_pretty(self).map_err(ConfigError::ToJson)
+
        serde_json::to_string_pretty(self).map_err(ConfigError::to_json)
    }
}

@@ -214,15 +213,15 @@ pub enum ConfigError {

    /// Can't parse config file as YAML.
    #[error("failed to parse configuration file as YAML: {0}")]
-
    ParseConfig(PathBuf, #[source] serde_norway::Error),
+
    ParseConfig(PathBuf, #[source] Box<dyn std::error::Error>),

    /// Can't parse config file as JSON.
    #[error("failed to parse configuration file as JSON: {0}")]
-
    ParseConfigJson(PathBuf, #[source] serde_json::Error),
+
    ParseConfigJson(PathBuf, #[source] Box<dyn std::error::Error>),

    /// Can't convert configuration into JSON.
    #[error("failed to convert configuration into JSON")]
-
    ToJson(#[source] serde_json::Error),
+
    ToJson(#[source] Box<dyn std::error::Error>),

    /// No default adapter.
    #[error(
@@ -235,6 +234,24 @@ pub enum ConfigError {
    UnknownAdapter(String),
}

+
impl ConfigError {
+
    fn read_config(path: &Path, err: std::io::Error) -> Self {
+
        Self::ReadConfig(path.into(), err)
+
    }
+

+
    fn parse_config(path: &Path, err: serde_norway::Error) -> Self {
+
        Self::ParseConfig(path.into(), Box::new(err))
+
    }
+

+
    fn parse_config_json(path: &Path, err: serde_json::Error) -> Self {
+
        Self::ParseConfigJson(path.into(), Box::new(err))
+
    }
+

+
    fn to_json(err: serde_json::Error) -> Self {
+
        Self::ToJson(Box::new(err))
+
    }
+
}
+

#[cfg(test)]
mod test {
    use super::*;
modified src/db.rs
@@ -554,176 +554,176 @@ impl QueuedCiEvent {
pub enum DbError {
    /// Error formatting a time as a string.
    #[error(transparent)]
-
    Timeformat(#[from] time::error::Format),
+
    Timeformat(#[from] Box<time::error::Format>),

    #[error("failed to set a busy timer one SQLite database {0}")]
-
    BusyTimer(PathBuf, #[source] sqlite::Error),
+
    BusyTimer(PathBuf, #[source] Box<sqlite::Error>),

    #[error("failed to open SQLite database {0}")]
-
    Open(PathBuf, #[source] sqlite::Error),
+
    Open(PathBuf, #[source] Box<sqlite::Error>),

    #[error("failed to prepare SQL statement SQLite database {0}: {1}")]
-
    Prepare(String, PathBuf, #[source] sqlite::Error),
+
    Prepare(String, PathBuf, #[source] Box<sqlite::Error>),

    #[error("failed to reset connection to SQLite")]
-
    Reset(#[source] sqlite::Error),
+
    Reset(#[source] Box<sqlite::Error>),

    #[error("failed to execute SQL statement in SQLite: {0}")]
-
    Execute(String, #[source] sqlite::Error),
+
    Execute(String, #[source] Box<sqlite::Error>),

    #[error("failed to bind a value in SQL statement in SQLite: {0}")]
-
    Bind(String, #[source] sqlite::Error),
+
    Bind(String, #[source] Box<sqlite::Error>),

    #[error("failed to read a column value output of SQL statement in SQLite: {0}")]
-
    Read(String, #[source] sqlite::Error),
+
    Read(String, #[source] Box<sqlite::Error>),

    #[error("failed to insert a counter into database")]
-
    InsertCounter(String, #[source] sqlite::Error),
+
    InsertCounter(String, #[source] Box<sqlite::Error>),

    #[error("failed to update a counter in database")]
-
    UpdateCounter(String, #[source] sqlite::Error),
+
    UpdateCounter(String, #[source] Box<sqlite::Error>),

    #[error("failed to retrieve a counter from database")]
-
    GetCounter(String, #[source] sqlite::Error),
+
    GetCounter(String, #[source] Box<sqlite::Error>),

    #[error("failed to insert an event into database")]
-
    InsertEvent(String, #[source] sqlite::Error),
+
    InsertEvent(String, #[source] Box<sqlite::Error>),

    #[error("failed to list queued events in database")]
-
    ListEvents(String, #[source] sqlite::Error),
+
    ListEvents(String, #[source] Box<sqlite::Error>),

    #[error("failed to retrieve a queued event in database")]
-
    GetEvent(String, #[source] sqlite::Error),
+
    GetEvent(String, #[source] Box<sqlite::Error>),

    #[error("failed to parse queued event as JSON: {0}")]
-
    EventFromJson(String, #[source] serde_json::Error),
+
    EventFromJson(String, #[source] Box<serde_json::Error>),

    /// Can't convert broker event into JSON.
    #[error("failed to convert broker event into JSON")]
-
    EventToJson(#[source] serde_json::Error),
+
    EventToJson(#[source] Box<serde_json::Error>),

    #[error("failed to insert an event into queue")]
-
    PushEvent(String, #[source] sqlite::Error),
+
    PushEvent(String, #[source] Box<sqlite::Error>),

    #[error("failed to remove an event from queue")]
-
    RemoveEvent(String, #[source] sqlite::Error),
+
    RemoveEvent(String, #[source] Box<sqlite::Error>),

    #[error("failed to list CI runs in database")]
-
    ListRuns(String, #[source] sqlite::Error),
+
    ListRuns(String, #[source] Box<sqlite::Error>),

    #[error("failed to get all CI runs from database")]
-
    GetAllRuns(String, #[source] sqlite::Error),
+
    GetAllRuns(String, #[source] Box<sqlite::Error>),

    #[error("failed to parse CI run as JSON: {0}")]
-
    RunFromJson(String, #[source] serde_json::Error),
+
    RunFromJson(String, #[source] Box<serde_json::Error>),

    #[error("failed to retrieve a CI run from database")]
-
    GetRun(String, #[source] sqlite::Error),
+
    GetRun(String, #[source] Box<sqlite::Error>),

    #[error("failed to insert a CI run into database")]
-
    PushRun(String, #[source] sqlite::Error),
+
    PushRun(String, #[source] Box<sqlite::Error>),

    #[error("failed to update a CI run in database")]
-
    UpdateRun(String, #[source] sqlite::Error),
+
    UpdateRun(String, #[source] Box<sqlite::Error>),

    #[error("failed to remove a CI run from database")]
-
    RemoveRun(String, #[source] sqlite::Error),
+
    RemoveRun(String, #[source] Box<sqlite::Error>),
}

impl DbError {
    fn time_format(e: time::error::Format) -> Self {
-
        Self::Timeformat(e)
+
        Self::Timeformat(Box::new(e))
    }

    fn busy_timer(filename: &Path, e: sqlite::Error) -> Self {
-
        Self::BusyTimer(filename.into(), e)
+
        Self::BusyTimer(filename.into(), Box::new(e))
    }

    fn open(filename: &Path, e: sqlite::Error) -> Self {
-
        Self::Open(filename.into(), e)
+
        Self::Open(filename.into(), Box::new(e))
    }

    fn prepare(sql: &str, filename: &Path, e: sqlite::Error) -> Self {
-
        Self::Prepare(sql.into(), filename.into(), e)
+
        Self::Prepare(sql.into(), filename.into(), Box::new(e))
    }

    fn reset(e: sqlite::Error) -> Self {
-
        Self::Reset(e)
+
        Self::Reset(Box::new(e))
    }

    fn execute(sql: &str, e: sqlite::Error) -> Self {
-
        Self::Execute(sql.into(), e)
+
        Self::Execute(sql.into(), Box::new(e))
    }

    fn bind(sql: &str, e: sqlite::Error) -> Self {
-
        Self::Bind(sql.into(), e)
+
        Self::Bind(sql.into(), Box::new(e))
    }

    fn read(sql: &str, e: sqlite::Error) -> Self {
-
        Self::Read(sql.into(), e)
+
        Self::Read(sql.into(), Box::new(e))
    }

    fn insert_counter(sql: &str, e: sqlite::Error) -> Self {
-
        Self::InsertCounter(sql.into(), e)
+
        Self::InsertCounter(sql.into(), Box::new(e))
    }

    fn update_counter(sql: &str, e: sqlite::Error) -> Self {
-
        Self::UpdateCounter(sql.into(), e)
+
        Self::UpdateCounter(sql.into(), Box::new(e))
    }

    fn get_counter(sql: &str, e: sqlite::Error) -> Self {
-
        Self::GetCounter(sql.into(), e)
+
        Self::GetCounter(sql.into(), Box::new(e))
    }

    fn list_events(sql: &str, e: sqlite::Error) -> Self {
-
        Self::ListEvents(sql.into(), e)
+
        Self::ListEvents(sql.into(), Box::new(e))
    }

    fn get_event(sql: &str, e: sqlite::Error) -> Self {
-
        Self::GetEvent(sql.into(), e)
+
        Self::GetEvent(sql.into(), Box::new(e))
    }

    fn event_from_json(json: &str, e: serde_json::Error) -> Self {
-
        Self::EventFromJson(json.into(), e)
+
        Self::EventFromJson(json.into(), Box::new(e))
    }

    fn event_to_json(e: serde_json::Error) -> Self {
-
        Self::EventToJson(e)
+
        Self::EventToJson(Box::new(e))
    }

    fn push_event(sql: &str, e: sqlite::Error) -> Self {
-
        Self::PushEvent(sql.into(), e)
+
        Self::PushEvent(sql.into(), Box::new(e))
    }

    fn remove_event(sql: &str, e: sqlite::Error) -> Self {
-
        Self::RemoveEvent(sql.into(), e)
+
        Self::RemoveEvent(sql.into(), Box::new(e))
    }

    fn list_runs(sql: &str, e: sqlite::Error) -> Self {
-
        Self::ListRuns(sql.into(), e)
+
        Self::ListRuns(sql.into(), Box::new(e))
    }

    fn get_all_runs(sql: &str, e: sqlite::Error) -> Self {
-
        Self::GetAllRuns(sql.into(), e)
+
        Self::GetAllRuns(sql.into(), Box::new(e))
    }

    fn run_from_json(json: &str, e: serde_json::Error) -> Self {
-
        Self::RunFromJson(json.into(), e)
+
        Self::RunFromJson(json.into(), Box::new(e))
    }

    fn get_run(sql: &str, e: sqlite::Error) -> Self {
-
        Self::GetRun(sql.into(), e)
+
        Self::GetRun(sql.into(), Box::new(e))
    }

    fn push_run(sql: &str, e: sqlite::Error) -> Self {
-
        Self::PushRun(sql.into(), e)
+
        Self::PushRun(sql.into(), Box::new(e))
    }

    fn update_run(sql: &str, e: sqlite::Error) -> Self {
-
        Self::UpdateRun(sql.into(), e)
+
        Self::UpdateRun(sql.into(), Box::new(e))
    }

    fn remove_run(sql: &str, e: sqlite::Error) -> Self {
-
        Self::RemoveRun(sql.into(), e)
+
        Self::RemoveRun(sql.into(), Box::new(e))
    }
}
modified src/filter.rs
@@ -432,8 +432,8 @@ impl Filters {
    fn from_file(filename: &Path) -> Result<Vec<EventFilter>, FilterError> {
        let data =
            std::fs::read(filename).map_err(|e| FilterError::ReadFile(filename.into(), e))?;
-
        let filters: Self = serde_norway::from_slice(&data)
-
            .map_err(|e| FilterError::ParseYaml(filename.into(), e))?;
+
        let filters: Self =
+
            serde_norway::from_slice(&data).map_err(|e| FilterError::parse_yaml(filename, e))?;
        Ok(filters.filters)
    }
}
@@ -1099,5 +1099,11 @@ pub enum FilterError {
    ReadFile(PathBuf, #[source] std::io::Error),

    #[error("failed to parse YAML event filters file {0}")]
-
    ParseYaml(PathBuf, #[source] serde_norway::Error),
+
    ParseYaml(PathBuf, #[source] Box<serde_norway::Error>),
+
}
+

+
impl FilterError {
+
    fn parse_yaml(path: &Path, err: serde_norway::Error) -> Self {
+
        Self::ParseYaml(path.into(), Box::new(err))
+
    }
}
modified src/logger.rs
@@ -191,7 +191,7 @@ pub enum LogLevelError {
    UnknownLogLevel(String),

    #[error("unknown log level {0:?}")]
-
    UnknownLogLevelValue(Value),
+
    UnknownLogLevelValue(String),
}

// We define our own type for log levels so that we can apply
@@ -231,7 +231,7 @@ impl TryFrom<&Value> for LogLevel {
    fn try_from(v: &Value) -> Result<Self, LogLevelError> {
        match v {
            Value::String(s) => Self::try_from(s.as_str()),
-
            _ => Err(LogLevelError::UnknownLogLevelValue(v.clone())),
+
            _ => Err(LogLevelError::UnknownLogLevelValue(v.to_string())),
        }
    }
}
modified src/msg.rs
@@ -146,25 +146,31 @@ impl<'a> RequestBuilder<'a> {
        fn repository(repo: &RepoId, profile: &Profile) -> Result<Repository, MessageError> {
            let rad_repo = match profile.storage.repository(*repo) {
                Err(err) => {
-
                    return Err(err)?;
+
                    return Err(MessageError::repository_error(err));
                }
                Ok(rad_repo) => rad_repo,
            };

            let project_info = match rad_repo.project() {
                Err(err) => {
-
                    return Err(err)?;
+
                    return Err(MessageError::repository_error(err));
                }
                Ok(x) => x,
            };

+
            let identity = rad_repo
+
                .identity()
+
                .map_err(MessageError::repository_error)?;
+
            let delegates = rad_repo
+
                .delegates()
+
                .map_err(MessageError::repository_error)?;
            Ok(Repository {
                id: *repo,
                name: project_info.name().to_string(),
                description: project_info.description().to_string(),
-
                private: !rad_repo.identity()?.visibility().is_public(),
+
                private: !identity.visibility().is_public(),
                default_branch: project_info.default_branch().to_string(),
-
                delegates: rad_repo.delegates()?.iter().copied().collect(),
+
                delegates: delegates.iter().copied().collect(),
            })
        }

@@ -209,14 +215,14 @@ impl<'a> RequestBuilder<'a> {
        ) -> Result<radicle::cob::patch::Patch, MessageError> {
            let x = match patch::Patches::open(rad_repo) {
                Err(err) => {
-
                    return Err(err)?;
+
                    return Err(MessageError::repository_error(err))?;
                }
                Ok(x) => x,
            };

            let x = match x.get(patch_id) {
                Err(err) => {
-
                    return Err(err)?;
+
                    return Err(MessageError::cob_store_error(err))?;
                }
                Ok(x) => x,
            };
@@ -296,8 +302,10 @@ impl<'a> RequestBuilder<'a> {
                old_tip,
            })) => {
                let git_repo =
-
                    radicle_surf::Repository::open(paths::repository(&profile.storage, repo))?;
-
                let mut commits = commits(&git_repo, *tip, *old_tip)?;
+
                    radicle_surf::Repository::open(paths::repository(&profile.storage, repo))
+
                        .map_err(MessageError::radicle_surf_error)?;
+
                let mut commits =
+
                    commits(&git_repo, *tip, *old_tip).map_err(MessageError::radicle_surf_error)?;
                if commits.is_empty() {
                    commits = vec![*old_tip];
                }
@@ -358,8 +366,10 @@ impl<'a> RequestBuilder<'a> {
                old_tip,
            })) => {
                let git_repo =
-
                    radicle_surf::Repository::open(paths::repository(&profile.storage, repo))?;
-
                let mut commits = commits(&git_repo, *tip, *old_tip)?;
+
                    radicle_surf::Repository::open(paths::repository(&profile.storage, repo))
+
                        .map_err(MessageError::radicle_surf_error)?;
+
                let mut commits =
+
                    commits(&git_repo, *tip, *old_tip).map_err(MessageError::radicle_surf_error)?;
                if commits.is_empty() {
                    commits = vec![*old_tip];
                }
@@ -400,14 +410,19 @@ impl<'a> RequestBuilder<'a> {
                patch: patch_id,
                new_tip,
            })) => {
-
                let rad_repo = profile.storage.repository(*repo)?;
+
                let rad_repo = profile
+
                    .storage
+
                    .repository(*repo)
+
                    .map_err(MessageError::repository_error)?;
                let git_repo =
-
                    radicle_surf::Repository::open(paths::repository(&profile.storage, repo))?;
+
                    radicle_surf::Repository::open(paths::repository(&profile.storage, repo))
+
                        .map_err(MessageError::radicle_surf_error)?;
                let author = author(from_node, profile)?;
                let patch_cob = patch_cob(&rad_repo, patch_id)?;
                let revisions = revisions(&patch_cob, &author)?;
                let patch_base = patch_base(&patch_cob, patch_id, &author)?;
-
                let commits = commits(&git_repo, *new_tip, patch_base)?;
+
                let commits = commits(&git_repo, *new_tip, patch_base)
+
                    .map_err(MessageError::radicle_surf_error)?;

                Ok(Request::Trigger {
                    common: common_fields(EventType::Patch, repo, profile)?,
@@ -428,7 +443,10 @@ impl<'a> RequestBuilder<'a> {
                            before: patch_base,
                            after: *new_tip,
                            commits,
-
                            target: patch_cob.target().head(&rad_repo)?,
+
                            target: patch_cob
+
                                .target()
+
                                .head(&rad_repo)
+
                                .map_err(MessageError::repository_error)?,
                            labels: patch_cob.labels().map(|l| l.name().to_string()).collect(),
                            assignees: patch_cob.assignees().collect(),
                            revisions,
@@ -442,14 +460,19 @@ impl<'a> RequestBuilder<'a> {
                patch: patch_id,
                new_tip,
            })) => {
-
                let rad_repo = profile.storage.repository(*repo)?;
+
                let rad_repo = profile
+
                    .storage
+
                    .repository(*repo)
+
                    .map_err(MessageError::repository_error)?;
                let git_repo =
-
                    radicle_surf::Repository::open(paths::repository(&profile.storage, repo))?;
+
                    radicle_surf::Repository::open(paths::repository(&profile.storage, repo))
+
                        .map_err(MessageError::radicle_surf_error)?;
                let author = author(from_node, profile)?;
                let patch_cob = patch_cob(&rad_repo, patch_id)?;
                let revisions = revisions(&patch_cob, &author)?;
                let patch_base = patch_base(&patch_cob, patch_id, &author)?;
-
                let commits = commits(&git_repo, *new_tip, patch_base)?;
+
                let commits = commits(&git_repo, *new_tip, patch_base)
+
                    .map_err(MessageError::radicle_surf_error)?;

                Ok(Request::Trigger {
                    common: common_fields(EventType::Patch, repo, profile)?,
@@ -470,7 +493,10 @@ impl<'a> RequestBuilder<'a> {
                            before: patch_base,
                            after: *new_tip,
                            commits,
-
                            target: patch_cob.target().head(&rad_repo)?,
+
                            target: patch_cob
+
                                .target()
+
                                .head(&rad_repo)
+
                                .map_err(MessageError::repository_error)?,
                            labels: patch_cob.labels().map(|l| l.name().to_string()).collect(),
                            assignees: patch_cob.assignees().collect(),
                            revisions,
@@ -548,13 +574,13 @@ impl Request {
    /// Serialize the request as a pretty JSON, including the newline.
    /// This is meant for the broker to use.
    pub fn to_json_pretty(&self) -> Result<String, MessageError> {
-
        serde_json::to_string_pretty(&self).map_err(MessageError::SerializeRequest)
+
        serde_json::to_string_pretty(&self).map_err(MessageError::serialize_request)
    }

    /// Serialize the request as a single-line JSON, including the
    /// newline. This is meant for the broker to use.
    pub fn to_writer<W: Write>(&self, mut writer: W) -> Result<(), MessageError> {
-
        let mut line = serde_json::to_string(&self).map_err(MessageError::SerializeRequest)?;
+
        let mut line = serde_json::to_string(&self).map_err(MessageError::serialize_request)?;
        line.push('\n');
        writer
            .write(line.as_bytes())
@@ -569,14 +595,14 @@ impl Request {
        let mut r = BufReader::new(reader);
        r.read_line(&mut line).map_err(MessageError::ReadLine)?;
        let req: Self =
-
            serde_json::from_slice(line.as_bytes()).map_err(MessageError::DeserializeRequest)?;
+
            serde_json::from_slice(line.as_bytes()).map_err(MessageError::deserialize_request)?;
        Ok(req)
    }

    /// Parse a request from a string. This is meant for tests to use.
    pub fn try_from_str(s: &str) -> Result<Self, MessageError> {
        let req: Self =
-
            serde_json::from_slice(s.as_bytes()).map_err(MessageError::DeserializeRequest)?;
+
            serde_json::from_slice(s.as_bytes()).map_err(MessageError::deserialize_request)?;
        Ok(req)
    }
}
@@ -860,7 +886,7 @@ impl Response {
    /// Serialize a response as a single-line JSON, including the
    /// newline. This is meant for the adapter to use.
    pub fn to_writer<W: Write>(&self, mut writer: W) -> Result<(), MessageError> {
-
        let mut line = serde_json::to_string(&self).map_err(MessageError::SerializeResponse)?;
+
        let mut line = serde_json::to_string(&self).map_err(MessageError::serialize_response)?;
        line.push('\n');
        writer
            .write(line.as_bytes())
@@ -871,7 +897,7 @@ impl Response {
    /// Serialize the response as a pretty JSON, including the newline.
    /// This is meant for the broker to use.
    pub fn to_json_pretty(&self) -> Result<String, MessageError> {
-
        serde_json::to_string_pretty(&self).map_err(MessageError::SerializeResponse)
+
        serde_json::to_string_pretty(&self).map_err(MessageError::serialize_response)
    }

    /// Read a response from a reader. This is meant for the broker to
@@ -885,7 +911,7 @@ impl Response {
            Ok(None)
        } else {
            let req: Self = serde_json::from_slice(line.as_bytes())
-
                .map_err(MessageError::DeserializeResponse)?;
+
                .map_err(MessageError::deserialize_response)?;
            Ok(Some(req))
        }
    }
@@ -895,7 +921,7 @@ impl Response {
    #[allow(clippy::should_implement_trait)]
    pub fn from_str(line: &str) -> Result<Self, MessageError> {
        let req: Self =
-
            serde_json::from_slice(line.as_bytes()).map_err(MessageError::DeserializeResponse)?;
+
            serde_json::from_slice(line.as_bytes()).map_err(MessageError::deserialize_response)?;
        Ok(req)
    }
}
@@ -934,12 +960,12 @@ pub enum MessageError {
    /// Failed to serialize a request message as JSON. This should
    /// never happen and likely indicates a programming failure.
    #[error("failed to serialize a request into JSON to a file handle")]
-
    SerializeRequest(#[source] serde_json::Error),
+
    SerializeRequest(#[source] Box<dyn std::error::Error + Send + 'static>),

    /// Failed to serialize a response message as JSON. This should never
    /// happen and likely indicates a programming failure.
    #[error("failed to serialize a request into JSON to a file handle")]
-
    SerializeResponse(#[source] serde_json::Error),
+
    SerializeResponse(#[source] Box<dyn std::error::Error + Send + 'static>),

    /// Failed to write the serialized request message to an open file.
    #[error("failed to write JSON to file handle")]
@@ -956,15 +982,11 @@ pub enum MessageError {

    /// Failed to parse JSON as a request or a response.
    #[error("failed to read a JSON request from a file handle")]
-
    DeserializeRequest(#[source] serde_json::Error),
+
    DeserializeRequest(#[source] Box<dyn std::error::Error + Send + 'static>),

    /// Failed to parse JSON as a response or a response.
    #[error("failed to read a JSON response from a file handle")]
-
    DeserializeResponse(#[source] serde_json::Error),
-

-
    /// Error from Radicle.
-
    #[error(transparent)]
-
    RadicleProfile(#[from] radicle::profile::Error),
+
    DeserializeResponse(#[source] Box<dyn std::error::Error + Send + 'static>),

    /// Error retrieving context to generate trigger.
    #[error("could not generate trigger from event")]
@@ -978,27 +1000,53 @@ pub enum MessageError {
    #[error("failed to look up latest revision for patch {0}")]
    LatestPatchRevision(PatchId),

-
    /// Error from Radicle storage.
-
    #[error(transparent)]
-
    StorageError(#[from] radicle::storage::Error),
-

    /// Error from Radicle repository.
-
    #[error(transparent)]
-
    RepositoryError(#[from] radicle::storage::RepositoryError),
+
    #[error("error from Radicle repository")]
+
    RepositoryError(#[source] Box<dyn std::error::Error + Send + 'static>),

    /// Error from Radicle COB.
-
    #[error(transparent)]
-
    CobStoreError(#[from] radicle::cob::store::Error),
+
    #[error("error from Radicle collaborative object")]
+
    CobStoreError(#[source] Box<dyn std::error::Error + Send + 'static>),

    /// Error from `radicle-surf` crate.
-
    #[error(transparent)]
-
    RadicleSurfError(#[from] radicle_surf::Error),
+
    #[error("error from radicle-surf")]
+
    RadicleSurfError(#[source] Box<dyn std::error::Error + Send + 'static>),

    /// Trying to create a PatchAction from an invalid value.
    #[error("invalid patch action {0:?}")]
    UnknownPatchAction(String),
}

+
impl MessageError {
+
    fn serialize_request(err: serde_json::Error) -> Self {
+
        Self::SerializeRequest(Box::new(err))
+
    }
+

+
    fn serialize_response(err: serde_json::Error) -> Self {
+
        Self::SerializeResponse(Box::new(err))
+
    }
+

+
    fn deserialize_request(err: serde_json::Error) -> Self {
+
        Self::DeserializeRequest(Box::new(err))
+
    }
+

+
    fn deserialize_response(err: serde_json::Error) -> Self {
+
        Self::DeserializeResponse(Box::new(err))
+
    }
+

+
    fn repository_error(err: radicle::storage::RepositoryError) -> Self {
+
        Self::RepositoryError(Box::new(err))
+
    }
+

+
    fn cob_store_error(err: radicle::cob::store::Error) -> Self {
+
        Self::CobStoreError(Box::new(err))
+
    }
+

+
    fn radicle_surf_error(err: radicle_surf::Error) -> Self {
+
        Self::RadicleSurfError(Box::new(err))
+
    }
+
}
+

#[cfg(test)]
#[allow(clippy::unwrap_used)] // OK in tests: panic is fine
#[allow(missing_docs)]
modified src/node_event_source.rs
@@ -1,6 +1,10 @@
//! Read node events from the local node.

-
use std::{fmt, path::PathBuf, time};
+
use std::{
+
    fmt,
+
    path::{Path, PathBuf},
+
    time,
+
};

use radicle::{
    Profile,
@@ -31,7 +35,7 @@ impl NodeEventSource {
            }),
            Err(err) => {
                logger::error("failed to subscribe to node events", &err);
-
                Err(NodeEventError::CannotSubscribe(socket.clone(), err))
+
                Err(NodeEventError::cannot_subscribe(&socket, err))
            }
        }?;
        logger::node_event_source_created(&source);
@@ -60,7 +64,7 @@ impl NodeEventSource {
                }
                Err(err) => {
                    logger::error("error reading event from node", &err);
-
                    Err(NodeEventError::Node(err))
+
                    Err(NodeEventError::node(err))
                }
            }
        } else {
@@ -79,47 +83,32 @@ impl fmt::Debug for NodeEventSource {
/// Possible errors from accessing the local Radicle node.
#[derive(Debug, thiserror::Error)]
pub enum NodeEventError {
-
    /// Regex compilation error.
-
    #[error("programming error in regular expression {0:?}")]
-
    Regex(&'static str, regex::Error),
-

    /// Node control socket does not exist.
    #[error("node control socket does not exist: {0}")]
    NoControlSocket(PathBuf),

    /// Can't subscribe to node events.
    #[error("failed to subscribe to node events on socket {0}")]
-
    CannotSubscribe(PathBuf, #[source] radicle::node::Error),
+
    CannotSubscribe(
+
        PathBuf,
+
        #[source] Box<dyn std::error::Error + Send + 'static>,
+
    ),

    /// Some error from getting an event from the node.
    #[error(transparent)]
-
    Node(#[from] radicle::node::Error),
+
    Node(#[from] Box<dyn std::error::Error + Send + 'static>),

    /// Connection to the node control socket broke.
    #[error("connection to the node control socket has been lost: can't continue")]
    BrokenConnection,
+
}

-
    /// Some error from parsing a repository id.
-
    #[error(transparent)]
-
    Id(#[from] radicle::identity::IdError),
-

-
    /// Some error doing input/output.
-
    #[error(transparent)]
-
    Io(#[from] std::io::Error),
-

-
    /// An error reading a filter file.
-
    #[error("failed to read filter file: {0}")]
-
    ReadFilterFile(PathBuf, #[source] std::io::Error),
-

-
    /// An error parsing JSON as filters, when read from a file.
-
    #[error("failed to parser filters file: {0}")]
-
    FiltersJsonFile(PathBuf, #[source] serde_json::Error),
-

-
    /// An error parsing JSON as filters, from an in-memory string.
-
    #[error("failed to parser filters as JSON")]
-
    FiltersJsonString(#[source] serde_json::Error),
+
impl NodeEventError {
+
    fn cannot_subscribe(path: &Path, err: radicle::node::Error) -> Self {
+
        Self::CannotSubscribe(path.into(), Box::new(err))
+
    }

-
    /// An error parsing a Git object id as string into an Oid.
-
    #[error("failed to parse string as a Git object id: {0:?}")]
-
    ParseOid(String, #[source] radicle::git::raw::Error),
+
    fn node(err: radicle::node::Error) -> Self {
+
        Self::Node(Box::new(err))
+
    }
}
modified src/pages.rs
@@ -48,8 +48,8 @@ const STATUS_JSON: &str = "status.json";
#[derive(Debug, thiserror::Error)]
pub enum PageError {
    /// Error formatting a time as a string.
-
    #[error(transparent)]
-
    Timeformat(#[from] time::error::Format),
+
    #[error("failed to format time as a string")]
+
    Timeformat(#[from] Box<time::error::Format>),

    #[error("failed to write status page to {0}")]
    Write(PathBuf, #[source] crate::util::UtilError),
@@ -67,15 +67,27 @@ pub enum PageError {
    Lock(&'static str),

    #[error("failed to represent status page data as JSON")]
-
    StatusToJson(#[source] serde_json::Error),
+
    StatusToJson(#[source] Box<serde_json::Error>),

    #[error("failed to create RSS time stamp from CI run timestamp: {0}")]
    RssTimestamp(String, #[source] crate::util::UtilError),
}

-
fn now() -> Result<String, time::error::Format> {
+
impl PageError {
+
    fn time_format(err: time::error::Format) -> Self {
+
        Self::Timeformat(Box::new(err))
+
    }
+

+
    fn status_to_json(err: serde_json::Error) -> Self {
+
        Self::StatusToJson(Box::new(err))
+
    }
+
}
+

+
fn now() -> Result<String, PageError> {
    let fmt = format_description!("[year]-[month]-[day] [hour]:[minute]:[second]Z");
-
    OffsetDateTime::now_utc().format(fmt)
+
    OffsetDateTime::now_utc()
+
        .format(fmt)
+
        .map_err(PageError::time_format)
}

struct PageData {
@@ -1049,7 +1061,7 @@ struct StatusData {

impl StatusData {
    fn as_json(&self) -> Result<String, PageError> {
-
        serde_json::to_string_pretty(self).map_err(PageError::StatusToJson)
+
        serde_json::to_string_pretty(self).map_err(PageError::status_to_json)
    }
}

modified src/queueadd.rs
@@ -41,7 +41,7 @@ pub struct QueueAdder {

impl QueueAdder {
    fn add_events(&self) -> Result<(), AdderError> {
-
        let profile = Profile::load()?;
+
        let profile = Profile::load().map_err(AdderError::profile)?;

        let mut source = CiEventSource::new(&profile)?;

@@ -90,7 +90,7 @@ pub enum AdderError {
    Missing(&'static str),

    #[error(transparent)]
-
    Profile(#[from] radicle::profile::Error),
+
    Profile(#[from] Box<radicle::profile::Error>),

    #[error(transparent)]
    CiEvent(#[from] CiEventSourceError),
@@ -101,3 +101,9 @@ pub enum AdderError {
    #[error("failed to notify other thread about database change")]
    Send,
}
+

+
impl AdderError {
+
    fn profile(err: radicle::profile::Error) -> Self {
+
        Self::Profile(Box::new(err))
+
    }
+
}
modified src/queueproc.rs
@@ -47,7 +47,7 @@ const DEFAULT_QUEUE_LEN_DURATION: Duration = Duration::from_secs(10);

impl QueueProcessorBuilder {
    pub fn build(self) -> Result<QueueProcessor, QueueError> {
-
        let profile = Profile::load().map_err(QueueError::Profile)?;
+
        let profile = Profile::load().map_err(QueueError::profile)?;
        let broker = self.broker.ok_or(QueueError::Missing("broker"))?;
        let filters = self.filters.ok_or(QueueError::Missing("filters"))?;
        let triggers = self.triggers.ok_or(QueueError::Missing("triggers"))?;
@@ -532,7 +532,7 @@ pub enum QueueError {
    KnownJobCobs(#[source] crate::cob::JobError),

    #[error("failed to load node profile")]
-
    Profile(#[source] radicle::profile::Error),
+
    Profile(#[source] Box<dyn std::error::Error + Send + 'static>),

    #[error("failed to open database")]
    OpenDb(#[source] crate::db::DbError),
@@ -595,4 +595,8 @@ impl QueueError {
    fn execute_ci(e: BrokerError) -> Self {
        Self::ExecuteCi(e)
    }
+

+
    fn profile(err: radicle::profile::Error) -> Self {
+
        Self::Profile(Box::new(err))
+
    }
}
modified src/refs.rs
@@ -57,9 +57,9 @@ impl TagName {
impl TryFrom<&str> for TagName {
    type Error = RefError;
    fn try_from(from: &str) -> Result<Self, RefError> {
-
        Ok(Self(RefString::try_from(from).map_err(|err| {
-
            RefError::RefStrCreate(from.to_string(), err)
-
        })?))
+
        Ok(Self(
+
            RefString::try_from(from).map_err(|err| RefError::ref_str_create(from, err))?,
+
        ))
    }
}

@@ -100,7 +100,7 @@ pub fn qualified_branch(name: &BranchName) -> Result<Qualified<'_>, RefError> {

/// Create a [`RefString`] from a [`String`].
pub fn ref_string(s: &str) -> Result<RefString, RefError> {
-
    RefString::try_from(s).map_err(|err| RefError::RefStrCreate(s.into(), err))
+
    RefString::try_from(s).map_err(|err| RefError::ref_str_create(s, err))
}

/// Create a name spaced branch name.
@@ -152,7 +152,7 @@ pub enum RefError {

    /// Failed to create a [`RefString`] from a string.
    #[error("failed to create a RefStr from a string {0:?}")]
-
    RefStrCreate(String, #[source] radicle::git::fmt::Error),
+
    RefStrCreate(String, #[source] Box<radicle::git::fmt::Error>),

    /// Can't create a [`Qualified`] value from a [`BranchName`].
    #[error("failed to create a qualified branch name from a branch ref {0:?}")]
@@ -175,6 +175,12 @@ pub enum RefError {
    PatchIdFromStr(String),
}

+
impl RefError {
+
    fn ref_str_create(s: &str, err: radicle::git::fmt::Error) -> Self {
+
        Self::RefStrCreate(s.into(), Box::new(err))
+
    }
+
}
+

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test {
modified src/util.rs
@@ -23,9 +23,9 @@ use radicle::{
};

pub fn lookup_repo(profile: &Profile, wanted: &str) -> Result<(RepoId, String), UtilError> {
-
    let storage = Storage::open(profile.storage(), profile.info()).map_err(UtilError::Storage)?;
+
    let storage = Storage::open(profile.storage(), profile.info()).map_err(UtilError::storage)?;

-
    let repos = storage.repositories().map_err(UtilError::Repositories)?;
+
    let repos = storage.repositories().map_err(UtilError::repositories)?;
    let mut rid = None;

    if let Ok(wanted_rid) = RepoId::from_urn(wanted) {
@@ -33,7 +33,7 @@ pub fn lookup_repo(profile: &Profile, wanted: &str) -> Result<(RepoId, String),
            let project = ri
                .doc
                .project()
-
                .map_err(|e| UtilError::Project(ri.rid, e))?;
+
                .map_err(|e| UtilError::project(ri.rid, e))?;

            if ri.rid == wanted_rid {
                if rid.is_some() {
@@ -47,7 +47,7 @@ pub fn lookup_repo(profile: &Profile, wanted: &str) -> Result<(RepoId, String),
            let project = ri
                .doc
                .project()
-
                .map_err(|e| UtilError::Project(ri.rid, e))?;
+
                .map_err(|e| UtilError::project(ri.rid, e))?;

            if project.name() == wanted {
                if rid.is_some() {
@@ -74,7 +74,7 @@ pub fn oid_from_cli_arg(profile: &Profile, rid: RepoId, commit: &str) -> Result<
}

pub fn load_profile() -> Result<Profile, UtilError> {
-
    Profile::load().map_err(UtilError::Profile)
+
    Profile::load().map_err(UtilError::profile)
}

pub fn lookup_nid(profile: &Profile) -> Result<NodeId, UtilError> {
@@ -82,14 +82,14 @@ pub fn lookup_nid(profile: &Profile) -> Result<NodeId, UtilError> {
}

pub fn lookup_commit(profile: &Profile, rid: RepoId, gitref: &str) -> Result<Oid, UtilError> {
-
    let storage = Storage::open(profile.storage(), profile.info()).map_err(UtilError::Storage)?;
+
    let storage = Storage::open(profile.storage(), profile.info()).map_err(UtilError::storage)?;
    let repo = storage
        .repository(rid)
-
        .map_err(|e| UtilError::RepoOpen(rid, e))?;
+
        .map_err(|e| UtilError::repo_open(rid, e))?;
    let object = repo
        .backend
        .revparse_single(gitref)
-
        .map_err(|e| UtilError::RevParse(gitref.into(), e))?;
+
        .map_err(|e| UtilError::rev_parse(gitref, e))?;

    Ok(object.id().into())
}
@@ -98,7 +98,7 @@ pub fn now() -> Result<String, UtilError> {
    let fmt = format_description!("[year]-[month]-[day] [hour]:[minute]:[second]Z");
    OffsetDateTime::now_utc()
        .format(fmt)
-
        .map_err(UtilError::TimeFormat)
+
        .map_err(UtilError::time_format)
}

pub fn parse_timestamp(timestamp: &str) -> Result<OffsetDateTime, UtilError> {
@@ -126,7 +126,7 @@ pub fn parse_timestamp(timestamp: &str) -> Result<OffsetDateTime, UtilError> {
}

pub fn rfc822_timestamp(ts: &OffsetDateTime) -> Result<String, UtilError> {
-
    let ts = ts.format(&Rfc2822).map_err(UtilError::TimeFormat)?;
+
    let ts = ts.format(&Rfc2822).map_err(UtilError::time_format)?;
    Ok(ts.to_string())
}

@@ -139,7 +139,7 @@ pub fn read_file_as_string(filename: &Path) -> Result<String, UtilError> {

pub fn read_file_as_objectid(filename: &Path) -> Result<ObjectId, UtilError> {
    let s = read_file_as_string(filename)?;
-
    ObjectId::from_str(s.trim()).map_err(|err| UtilError::ReadObjectId(filename.into(), err))
+
    ObjectId::from_str(s.trim()).map_err(|err| UtilError::read_object_id(filename, err))
}

pub fn safely_overwrite<P: AsRef<Path>>(filename: P, data: &[u8]) -> Result<(), UtilError> {
@@ -154,23 +154,23 @@ pub fn safely_overwrite<P: AsRef<Path>>(filename: P, data: &[u8]) -> Result<(),
    let mode = Permissions::from_mode(0o644);
    std::fs::set_permissions(tmp.path(), mode).map_err(UtilError::TempPerm)?;
    tmp.persist(filename)
-
        .map_err(|err| UtilError::RenameTemp(filename.to_path_buf(), err))?;
+
        .map_err(|err| UtilError::rename_temp(filename, err))?;
    Ok(())
}

#[derive(Debug, thiserror::Error)]
pub enum UtilError {
    #[error("failed to look up node profile")]
-
    Profile(#[source] radicle::profile::Error),
+
    Profile(#[source] Box<radicle::profile::Error>),

    #[error("failed to look up open node storage")]
-
    Storage(#[source] radicle::storage::Error),
+
    Storage(#[source] Box<radicle::storage::Error>),

    #[error("failed to list repositories in node storage")]
-
    Repositories(#[source] radicle::storage::Error),
+
    Repositories(#[source] Box<radicle::storage::Error>),

    #[error("failed to look up project info for repository {0}")]
-
    Project(RepoId, #[source] radicle::identity::doc::PayloadError),
+
    Project(RepoId, #[source] Box<radicle::identity::doc::PayloadError>),

    #[error("node has more than one repository called {0}")]
    DuplicateRepositories(String),
@@ -179,13 +179,13 @@ pub enum UtilError {
    NotFound(String),

    #[error("failed to open git repository in node storage: {0}")]
-
    RepoOpen(RepoId, #[source] radicle::storage::RepositoryError),
+
    RepoOpen(RepoId, #[source] Box<radicle::storage::RepositoryError>),

    #[error("failed to parse git ref as a commit id: {0}")]
-
    RevParse(String, #[source] radicle::git::raw::Error),
+
    RevParse(String, #[source] Box<radicle::git::raw::Error>),

    #[error("failed to format time stamp")]
-
    TimeFormat(#[source] time::error::Format),
+
    TimeFormat(#[source] Box<time::error::Format>),

    #[error("failed to parse timestamp {0:?}")]
    TimestampParse(String),
@@ -197,7 +197,7 @@ pub enum UtilError {
    Utf8(PathBuf, #[source] std::string::FromUtf8Error),

    #[error("failed to read object id from {0}")]
-
    ReadObjectId(PathBuf, #[source] radicle::cob::object::ParseObjectId),
+
    ReadObjectId(PathBuf, #[source] Box<radicle::cob::object::ParseObjectId>),

    #[error("file name to write to doesn't have a parent directory: {0}")]
    NoParent(PathBuf),
@@ -212,5 +212,43 @@ pub enum UtilError {
    TempPerm(#[source] std::io::Error),

    #[error("failed to rename temporary file to {0}")]
-
    RenameTemp(PathBuf, #[source] tempfile::PersistError),
+
    RenameTemp(PathBuf, #[source] Box<tempfile::PersistError>),
+
}
+

+
impl UtilError {
+
    fn profile(err: radicle::profile::Error) -> Self {
+
        Self::Profile(Box::new(err))
+
    }
+

+
    fn storage(err: radicle::storage::Error) -> Self {
+
        Self::Storage(Box::new(err))
+
    }
+

+
    fn repositories(err: radicle::storage::Error) -> Self {
+
        Self::Repositories(Box::new(err))
+
    }
+

+
    fn project(repo_id: RepoId, err: radicle::identity::doc::PayloadError) -> Self {
+
        Self::Project(repo_id, Box::new(err))
+
    }
+

+
    fn repo_open(repo_id: RepoId, err: radicle::storage::RepositoryError) -> Self {
+
        Self::RepoOpen(repo_id, Box::new(err))
+
    }
+

+
    fn rev_parse(s: &str, err: radicle::git::raw::Error) -> Self {
+
        Self::RevParse(s.into(), Box::new(err))
+
    }
+

+
    fn time_format(err: time::error::Format) -> Self {
+
        Self::TimeFormat(Box::new(err))
+
    }
+

+
    fn read_object_id(path: &Path, err: radicle::cob::object::ParseObjectId) -> Self {
+
        Self::ReadObjectId(path.into(), Box::new(err))
+
    }
+

+
    fn rename_temp(path: &Path, err: tempfile::PersistError) -> Self {
+
        Self::RenameTemp(path.into(), Box::new(err))
+
    }
}