Skip to main content

act_types/
types.rs

1use std::collections::HashMap;
2
3use crate::cbor;
4
5// ── LocalizedString ──
6
7/// A localizable text value, matching the WIT `localized-string` variant.
8///
9/// - `Plain` — a single string in the component's `default-language`.
10/// - `Localized` — a map of BCP 47 language tags to text.
11#[derive(Debug, Clone)]
12pub enum LocalizedString {
13    /// A single string assumed to be in the component's `default-language`.
14    Plain(String),
15    /// Language tag → text map. MUST include the component's `default-language`.
16    Localized(HashMap<String, String>),
17}
18
19impl Default for LocalizedString {
20    fn default() -> Self {
21        Self::Plain(String::new())
22    }
23}
24
25impl LocalizedString {
26    /// Create a plain (non-localized) string.
27    pub fn plain(text: impl Into<String>) -> Self {
28        Self::Plain(text.into())
29    }
30
31    /// Create a localized string with a single language entry.
32    pub fn new(lang: impl Into<String>, text: impl Into<String>) -> Self {
33        let mut map = HashMap::new();
34        map.insert(lang.into(), text.into());
35        Self::Localized(map)
36    }
37
38    /// Look up text for a specific language tag.
39    ///
40    /// For `Plain`, always returns the text (it is assumed to match any language).
41    /// For `Localized`, performs exact key lookup.
42    pub fn get(&self, lang: &str) -> Option<&str> {
43        match self {
44            Self::Plain(text) => Some(text.as_str()),
45            Self::Localized(map) => map.get(lang).map(|s| s.as_str()),
46        }
47    }
48
49    /// Resolve to text for the given language, with fallback chain.
50    ///
51    /// - `Plain` → returns the plain string (assumed to be in `default_language`).
52    /// - `Localized` → exact match → prefix match → any entry.
53    pub fn resolve(&self, lang: &str) -> &str {
54        match self {
55            Self::Plain(text) => text.as_str(),
56            Self::Localized(map) => {
57                // 1. Exact match
58                if let Some(text) = map.get(lang) {
59                    return text.as_str();
60                }
61                // 2. Prefix match (e.g. "zh" matches "zh-Hans")
62                if let Some(text) = map
63                    .iter()
64                    .find(|(tag, _)| tag.starts_with(lang) || lang.starts_with(tag.as_str()))
65                    .map(|(_, text)| text.as_str())
66                {
67                    return text;
68                }
69                // 3. Any entry
70                map.values().next().map(|s| s.as_str()).unwrap_or("")
71            }
72        }
73    }
74
75    /// Get some text, regardless of language.
76    /// Useful when you don't have the default language available.
77    pub fn any_text(&self) -> &str {
78        match self {
79            Self::Plain(text) => text.as_str(),
80            Self::Localized(map) => map.values().next().map(|s| s.as_str()).unwrap_or(""),
81        }
82    }
83}
84
85impl From<String> for LocalizedString {
86    fn from(s: String) -> Self {
87        Self::Plain(s)
88    }
89}
90
91impl From<&str> for LocalizedString {
92    fn from(s: &str) -> Self {
93        Self::Plain(s.to_string())
94    }
95}
96
97impl From<Vec<(String, String)>> for LocalizedString {
98    fn from(v: Vec<(String, String)>) -> Self {
99        Self::Localized(v.into_iter().collect())
100    }
101}
102
103impl From<HashMap<String, String>> for LocalizedString {
104    fn from(map: HashMap<String, String>) -> Self {
105        Self::Localized(map)
106    }
107}
108
109// ── Metadata ──
110
111/// Key → value metadata, stored as JSON values internally.
112///
113/// Converts to/from WIT `list<tuple<string, list<u8>>>` (CBOR) at the boundary.
114#[derive(Debug, Clone, Default)]
115pub struct Metadata(HashMap<String, serde_json::Value>);
116
117impl Metadata {
118    pub fn new() -> Self {
119        Self(HashMap::new())
120    }
121
122    /// Insert a value. Overwrites any existing entry for the key.
123    pub fn insert(&mut self, key: impl Into<String>, value: impl Into<serde_json::Value>) {
124        self.0.insert(key.into(), value.into());
125    }
126
127    /// Get a value by key.
128    pub fn get(&self, key: &str) -> Option<&serde_json::Value> {
129        self.0.get(key)
130    }
131
132    /// Get a value by key, deserializing into a typed value.
133    pub fn get_as<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
134        self.0
135            .get(key)
136            .and_then(|v| serde_json::from_value(v.clone()).ok())
137    }
138
139    /// Check if a key exists.
140    pub fn contains_key(&self, key: &str) -> bool {
141        self.0.contains_key(key)
142    }
143
144    /// Returns true if there are no entries.
145    pub fn is_empty(&self) -> bool {
146        self.0.is_empty()
147    }
148
149    /// Iterate over key-value pairs.
150    pub fn iter(&self) -> impl Iterator<Item = (&String, &serde_json::Value)> {
151        self.0.iter()
152    }
153
154    /// Convert to a JSON object. Returns `None` if empty.
155    pub fn to_json(&self) -> Option<serde_json::Value> {
156        if self.0.is_empty() {
157            return None;
158        }
159        let map: serde_json::Map<String, serde_json::Value> =
160            self.0.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
161        Some(serde_json::Value::Object(map))
162    }
163}
164
165/// Convert from WIT metadata (CBOR-encoded values).
166impl From<Vec<(String, Vec<u8>)>> for Metadata {
167    fn from(v: Vec<(String, Vec<u8>)>) -> Self {
168        Self(
169            v.into_iter()
170                .filter_map(|(k, cbor_bytes)| {
171                    let val = cbor::cbor_to_json(&cbor_bytes).ok()?;
172                    Some((k, val))
173                })
174                .collect(),
175        )
176    }
177}
178
179/// Convert to WIT metadata (CBOR-encoded values).
180impl From<Metadata> for Vec<(String, Vec<u8>)> {
181    fn from(m: Metadata) -> Self {
182        m.0.into_iter()
183            .map(|(k, v)| (k, cbor::to_cbor(&v)))
184            .collect()
185    }
186}
187
188// ── CBOR byte wrappers ──
189
190/// Shared implementation for CBOR byte wrapper types.
191macro_rules! cbor_wrapper {
192    ($(#[$meta:meta])* $name:ident) => {
193        $(#[$meta])*
194        #[derive(Debug, Clone)]
195        pub struct $name(Vec<u8>);
196
197        impl $name {
198            /// Encode a JSON value to CBOR.
199            pub fn from_json(value: &serde_json::Value) -> Result<Self, cbor::CborError> {
200                cbor::json_to_cbor(value).map(Self)
201            }
202
203            /// Convert from an optional JSON value.
204            pub fn from_json_opt(value: &Option<serde_json::Value>) -> Result<Option<Self>, cbor::CborError> {
205                match value {
206                    Some(val) => Self::from_json(val).map(Some),
207                    None => Ok(None),
208                }
209            }
210
211            /// Get the raw CBOR bytes.
212            pub fn as_bytes(&self) -> &[u8] {
213                &self.0
214            }
215
216            /// Decode to a JSON value.
217            pub fn to_json(&self) -> Result<serde_json::Value, cbor::CborError> {
218                cbor::cbor_to_json(&self.0)
219            }
220
221            /// Deserialize into a typed value.
222            pub fn deserialize<T: serde::de::DeserializeOwned>(&self) -> Result<T, cbor::CborError> {
223                cbor::from_cbor(&self.0)
224            }
225        }
226
227        impl From<Vec<u8>> for $name {
228            fn from(v: Vec<u8>) -> Self {
229                Self(v)
230            }
231        }
232
233        impl From<$name> for Vec<u8> {
234            fn from(w: $name) -> Self {
235                w.0
236            }
237        }
238    };
239}
240
241cbor_wrapper!(
242    /// Tool arguments as CBOR bytes.
243    Args
244);
245
246cbor_wrapper!(
247    /// Component configuration as CBOR bytes.
248    Config
249);
250
251use crate::constants::*;
252
253// ── Error type ──
254
255/// Error type mapping to ACT `tool-error`.
256#[derive(Debug, Clone)]
257pub struct ActError {
258    pub kind: String,
259    pub message: String,
260}
261
262impl ActError {
263    pub fn new(kind: impl Into<String>, message: impl Into<String>) -> Self {
264        Self {
265            kind: kind.into(),
266            message: message.into(),
267        }
268    }
269
270    pub fn not_found(message: impl Into<String>) -> Self {
271        Self::new(ERR_NOT_FOUND, message)
272    }
273
274    pub fn invalid_args(message: impl Into<String>) -> Self {
275        Self::new(ERR_INVALID_ARGS, message)
276    }
277
278    pub fn internal(message: impl Into<String>) -> Self {
279        Self::new(ERR_INTERNAL, message)
280    }
281
282    pub fn timeout(message: impl Into<String>) -> Self {
283        Self::new(ERR_TIMEOUT, message)
284    }
285
286    pub fn capability_denied(message: impl Into<String>) -> Self {
287        Self::new(ERR_CAPABILITY_DENIED, message)
288    }
289}
290
291impl std::fmt::Display for ActError {
292    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
293        write!(f, "{}: {}", self.kind, self.message)
294    }
295}
296
297impl std::error::Error for ActError {}
298
299/// Result type for ACT operations.
300pub type ActResult<T> = Result<T, ActError>;
301
302#[cfg(test)]
303mod tests {
304    use super::*;
305    use serde_json::json;
306
307    #[test]
308    fn localized_string_plain() {
309        let ls = LocalizedString::plain("hello");
310        assert_eq!(ls.resolve("en"), "hello");
311        assert_eq!(ls.any_text(), "hello");
312    }
313
314    #[test]
315    fn localized_string_from_str() {
316        let ls = LocalizedString::from("hello");
317        assert_eq!(ls.any_text(), "hello");
318    }
319
320    #[test]
321    fn localized_string_default() {
322        let ls = LocalizedString::default();
323        assert_eq!(ls.any_text(), "");
324    }
325
326    #[test]
327    fn localized_string_resolve_by_lang() {
328        let mut map = std::collections::HashMap::new();
329        map.insert("en".to_string(), "hello".to_string());
330        map.insert("ru".to_string(), "привет".to_string());
331        let ls = LocalizedString::Localized(map);
332        assert_eq!(ls.resolve("ru"), "привет");
333        assert_eq!(ls.resolve("en"), "hello");
334        // Unknown lang falls back to some entry
335        assert!(!ls.resolve("fr").is_empty());
336    }
337
338    #[test]
339    fn localized_string_resolve_prefix() {
340        let mut map = HashMap::new();
341        map.insert("zh-Hans".to_string(), "你好".to_string());
342        map.insert("en".to_string(), "hello".to_string());
343        let ls = LocalizedString::Localized(map);
344        assert_eq!(ls.resolve("zh"), "你好");
345    }
346
347    #[test]
348    fn localized_string_get() {
349        let ls = LocalizedString::new("en", "hello");
350        assert_eq!(ls.get("en"), Some("hello"));
351        assert_eq!(ls.get("ru"), None);
352    }
353
354    #[test]
355    fn localized_string_from_vec() {
356        let v = vec![("en".to_string(), "hi".to_string())];
357        let ls = LocalizedString::from(v);
358        assert_eq!(ls.resolve("en"), "hi");
359    }
360
361    #[test]
362    fn metadata_insert_and_get() {
363        let mut m = Metadata::new();
364        m.insert("std:read-only", true);
365        assert_eq!(m.get("std:read-only"), Some(&json!(true)));
366        assert_eq!(m.get_as::<bool>("std:read-only"), Some(true));
367    }
368
369    #[test]
370    fn metadata_to_json_empty() {
371        assert!(Metadata::new().to_json().is_none());
372    }
373
374    #[test]
375    fn metadata_to_json_with_values() {
376        let mut m = Metadata::new();
377        m.insert("std:read-only", true);
378        let json = m.to_json().unwrap();
379        assert_eq!(json["std:read-only"], json!(true));
380    }
381
382    #[test]
383    fn metadata_from_vec() {
384        let v = vec![("key".to_string(), cbor::to_cbor(&42u32))];
385        let m = Metadata::from(v);
386        assert_eq!(m.get("key"), Some(&json!(42)));
387        assert_eq!(m.get_as::<u32>("key"), Some(42));
388    }
389
390    #[test]
391    fn args_from_json_roundtrip() {
392        let val = json!({"code": "2+2"});
393        let args = Args::from_json(&val).unwrap();
394        let decoded = args.to_json().unwrap();
395        assert_eq!(val, decoded);
396    }
397
398    #[test]
399    fn args_deserialize_typed() {
400        #[derive(serde::Deserialize, PartialEq, Debug)]
401        struct Params {
402            code: String,
403        }
404        let val = json!({"code": "hello"});
405        let args = Args::from_json(&val).unwrap();
406        let params: Params = args.deserialize().unwrap();
407        assert_eq!(params.code, "hello");
408    }
409
410    #[test]
411    fn config_from_json_opt_none() {
412        assert!(Config::from_json_opt(&None).unwrap().is_none());
413    }
414
415    #[test]
416    fn config_from_json_opt_some() {
417        let val = json!({"key": "value"});
418        let config = Config::from_json_opt(&Some(val.clone())).unwrap().unwrap();
419        let decoded = config.to_json().unwrap();
420        assert_eq!(decoded, val);
421    }
422}