execra 1.1.0

Typed job runtime for Rust apps that wrap external CLI tools: events, interpreters, cancellation, Tauri integration, and optional persistence.
Documentation
use std::path::PathBuf;
use std::time::SystemTime;

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
    Info,
    Recommendation,
    Warning,
    Error,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Action {
    Command {
        label: String,
        program: String,
        args: Vec<String>,
        cwd: Option<PathBuf>,
    },
    Link {
        label: String,
        url: String,
    },
    Instruction {
        label: String,
        text: String,
    },
}

impl Action {
    pub fn command<I, S>(label: impl Into<String>, program: impl Into<String>, args: I) -> Self
    where
        I: IntoIterator<Item = S>,
        S: Into<String>,
    {
        Action::Command {
            label: label.into(),
            program: program.into(),
            args: args.into_iter().map(Into::into).collect(),
            cwd: None,
        }
    }

    pub fn command_in<I, S>(
        label: impl Into<String>,
        program: impl Into<String>,
        args: I,
        cwd: impl Into<PathBuf>,
    ) -> Self
    where
        I: IntoIterator<Item = S>,
        S: Into<String>,
    {
        Action::Command {
            label: label.into(),
            program: program.into(),
            args: args.into_iter().map(Into::into).collect(),
            cwd: Some(cwd.into()),
        }
    }

    pub fn link(label: impl Into<String>, url: impl Into<String>) -> Self {
        Action::Link {
            label: label.into(),
            url: url.into(),
        }
    }

    pub fn instruction(label: impl Into<String>, text: impl Into<String>) -> Self {
        Action::Instruction {
            label: label.into(),
            text: text.into(),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
pub enum RelatedEntity {
    Package(String),
    File(PathBuf),
    Url(String),
    Other(String),
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Finding {
    pub severity: Severity,
    pub code: String,
    pub message: String,
    pub action: Option<Action>,
    pub related: Option<RelatedEntity>,
    pub at: SystemTime,
}

impl Finding {
    fn make(severity: Severity, code: impl Into<String>, message: impl Into<String>) -> Self {
        Finding {
            severity,
            code: code.into(),
            message: message.into(),
            action: None,
            related: None,
            at: SystemTime::now(),
        }
    }

    pub fn info(code: impl Into<String>, message: impl Into<String>) -> Self {
        Self::make(Severity::Info, code, message)
    }

    pub fn recommendation(code: impl Into<String>, message: impl Into<String>) -> Self {
        Self::make(Severity::Recommendation, code, message)
    }

    pub fn warning(code: impl Into<String>, message: impl Into<String>) -> Self {
        Self::make(Severity::Warning, code, message)
    }

    pub fn error(code: impl Into<String>, message: impl Into<String>) -> Self {
        Self::make(Severity::Error, code, message)
    }

    pub fn with_action(mut self, action: Action) -> Self {
        self.action = Some(action);
        self
    }

    pub fn with_related(mut self, related: RelatedEntity) -> Self {
        self.related = Some(related);
        self
    }
}