Skip to main content

a3s_use_core/
lib.rs

1use std::collections::BTreeMap;
2use std::fmt;
3use std::path::PathBuf;
4
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
8#[serde(transparent)]
9pub struct UseSessionId(String);
10
11impl UseSessionId {
12    pub fn parse(value: impl Into<String>) -> Result<Self, UseError> {
13        let value = value.into();
14        if value.is_empty()
15            || !value.chars().all(|character| {
16                character.is_ascii_alphanumeric() || matches!(character, '-' | '_')
17            })
18        {
19            return Err(UseError::new(
20                "use.session.invalid_id",
21                "Session IDs may contain only ASCII letters, digits, '-' and '_'.",
22            ));
23        }
24        Ok(Self(value))
25    }
26
27    pub fn as_str(&self) -> &str {
28        &self.0
29    }
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
33#[serde(rename_all = "kebab-case")]
34pub enum RiskClass {
35    Read,
36    Navigate,
37    Mutate,
38    Submit,
39    Download,
40    Execute,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
44#[serde(rename_all = "camelCase")]
45pub struct Artifact {
46    pub path: PathBuf,
47    pub media_type: String,
48    pub size: u64,
49    pub sha256: String,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
53#[serde(rename_all = "kebab-case")]
54pub enum Readiness {
55    Ready,
56    Missing,
57    Broken,
58    Unknown,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62#[serde(rename_all = "camelCase")]
63pub struct DomainDiagnostic {
64    pub domain: String,
65    pub readiness: Readiness,
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub provider: Option<String>,
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub version: Option<String>,
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub path: Option<PathBuf>,
72    pub message: String,
73    #[serde(default, skip_serializing_if = "Vec::is_empty")]
74    pub suggestions: Vec<String>,
75}
76
77#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
78#[serde(rename_all = "camelCase")]
79pub struct UseError {
80    pub code: String,
81    pub message: String,
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub suggestion: Option<String>,
84    #[serde(default)]
85    pub details: BTreeMap<String, serde_json::Value>,
86}
87
88impl UseError {
89    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
90        Self {
91            code: code.into(),
92            message: message.into(),
93            suggestion: None,
94            details: BTreeMap::new(),
95        }
96    }
97
98    pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
99        self.suggestion = Some(suggestion.into());
100        self
101    }
102
103    pub fn with_detail(
104        mut self,
105        name: impl Into<String>,
106        value: impl Into<serde_json::Value>,
107    ) -> Self {
108        self.details.insert(name.into(), value.into());
109        self
110    }
111}
112
113impl fmt::Display for UseError {
114    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
115        formatter.write_str(&self.message)
116    }
117}
118
119impl std::error::Error for UseError {}
120
121pub type UseResult<T> = Result<T, UseError>;
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn session_ids_are_bounded_and_serializable() {
129        let id = UseSessionId::parse("browser_01").unwrap();
130        assert_eq!(id.as_str(), "browser_01");
131        assert!(UseSessionId::parse("../escape").is_err());
132        assert_eq!(serde_json::to_string(&id).unwrap(), "\"browser_01\"");
133    }
134
135    #[test]
136    fn stable_error_has_machine_code_and_suggestion() {
137        let error = UseError::new("use.runtime.missing", "Runtime is missing.")
138            .with_suggestion("Run a3s install use/browser.");
139        let value = serde_json::to_value(error).unwrap();
140        assert_eq!(value["code"], "use.runtime.missing");
141        assert!(value["suggestion"]
142            .as_str()
143            .unwrap()
144            .contains("a3s install"));
145    }
146}