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}