1use std::collections::{BTreeMap, HashMap};
2
3use crate::cbor;
4
5#[derive(Debug, Clone)]
12pub enum LocalizedString {
13 Plain(String),
15 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 pub fn plain(text: impl Into<String>) -> Self {
28 Self::Plain(text.into())
29 }
30
31 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 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 pub fn resolve(&self, lang: &str) -> &str {
54 match self {
55 Self::Plain(text) => text.as_str(),
56 Self::Localized(map) => {
57 if let Some(text) = map.get(lang) {
59 return text.as_str();
60 }
61 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 map.values().next().map(|s| s.as_str()).unwrap_or("")
71 }
72 }
73 }
74
75 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#[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 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 pub fn get(&self, key: &str) -> Option<&serde_json::Value> {
129 self.0.get(key)
130 }
131
132 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 pub fn contains_key(&self, key: &str) -> bool {
141 self.0.contains_key(key)
142 }
143
144 pub fn is_empty(&self) -> bool {
146 self.0.is_empty()
147 }
148
149 pub fn iter(&self) -> impl Iterator<Item = (&String, &serde_json::Value)> {
151 self.0.iter()
152 }
153
154 pub fn len(&self) -> usize {
156 self.0.len()
157 }
158
159 pub fn extend(&mut self, other: Metadata) {
161 self.0.extend(other.0);
162 }
163}
164
165impl From<serde_json::Value> for Metadata {
167 fn from(value: serde_json::Value) -> Self {
168 match value {
169 serde_json::Value::Object(map) => Self(map.into_iter().collect()),
170 _ => Self::new(),
171 }
172 }
173}
174
175impl From<Metadata> for serde_json::Value {
177 fn from(m: Metadata) -> Self {
178 serde_json::Value::Object(m.0.into_iter().collect())
179 }
180}
181
182impl From<Vec<(String, Vec<u8>)>> for Metadata {
184 fn from(v: Vec<(String, Vec<u8>)>) -> Self {
185 Self(
186 v.into_iter()
187 .filter_map(|(k, cbor_bytes)| {
188 let val = cbor::cbor_to_json(&cbor_bytes).ok()?;
189 Some((k, val))
190 })
191 .collect(),
192 )
193 }
194}
195
196impl From<Metadata> for Vec<(String, Vec<u8>)> {
198 fn from(m: Metadata) -> Self {
199 m.0.into_iter()
200 .map(|(k, v)| (k, cbor::to_cbor(&v)))
201 .collect()
202 }
203}
204
205use crate::constants::*;
206
207#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
211pub struct FilesystemCap {
212 #[serde(
214 rename = "mount-root",
215 default,
216 skip_serializing_if = "Option::is_none"
217 )]
218 pub mount_root: Option<String>,
219}
220
221#[derive(Debug, Clone, Copy, Default, serde::Serialize, serde::Deserialize)]
223pub struct HttpCap {}
224
225#[derive(Debug, Clone, Copy, Default, serde::Serialize, serde::Deserialize)]
227pub struct SocketsCap {}
228
229#[serde_with::skip_serializing_none]
234#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
235#[serde(default)]
236pub struct Capabilities {
237 #[serde(rename = "wasi:filesystem")]
239 pub filesystem: Option<FilesystemCap>,
240 #[serde(rename = "wasi:http")]
242 pub http: Option<HttpCap>,
243 #[serde(rename = "wasi:sockets")]
245 pub sockets: Option<SocketsCap>,
246 #[serde(flatten)]
248 pub other: BTreeMap<String, serde_json::Value>,
249}
250
251impl Capabilities {
252 pub fn is_empty(&self) -> bool {
254 self.http.is_none()
255 && self.filesystem.is_none()
256 && self.sockets.is_none()
257 && self.other.is_empty()
258 }
259
260 pub fn has(&self, id: &str) -> bool {
262 match id {
263 CAP_HTTP => self.http.is_some(),
264 CAP_FILESYSTEM => self.filesystem.is_some(),
265 CAP_SOCKETS => self.sockets.is_some(),
266 other => self.other.contains_key(other),
267 }
268 }
269
270 pub fn fs_mount_root(&self) -> Option<&str> {
272 self.filesystem.as_ref()?.mount_root.as_deref()
273 }
274}
275
276#[non_exhaustive]
286#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
287pub struct ComponentInfo {
288 #[serde(rename = "std:name", alias = "name", default)]
289 pub name: String,
290 #[serde(rename = "std:version", alias = "version", default)]
291 pub version: String,
292 #[serde(rename = "std:description", alias = "description", default)]
293 pub description: String,
294 #[serde(
295 rename = "std:default-language",
296 alias = "default-language",
297 default,
298 skip_serializing_if = "Option::is_none"
299 )]
300 pub default_language: Option<String>,
301 #[serde(
302 rename = "std:capabilities",
303 alias = "capabilities",
304 default,
305 skip_serializing_if = "Capabilities::is_empty"
306 )]
307 pub capabilities: Capabilities,
308 #[serde(flatten, default, skip_serializing_if = "HashMap::is_empty")]
310 pub metadata: HashMap<String, serde_json::Value>,
311}
312
313impl ComponentInfo {
314 pub fn new(
315 name: impl Into<String>,
316 version: impl Into<String>,
317 description: impl Into<String>,
318 ) -> Self {
319 Self {
320 name: name.into(),
321 version: version.into(),
322 description: description.into(),
323 ..Default::default()
324 }
325 }
326}
327
328#[derive(Debug, Clone)]
332pub struct ActError {
333 pub kind: String,
334 pub message: String,
335}
336
337impl ActError {
338 pub fn new(kind: impl Into<String>, message: impl Into<String>) -> Self {
339 Self {
340 kind: kind.into(),
341 message: message.into(),
342 }
343 }
344
345 pub fn not_found(message: impl Into<String>) -> Self {
346 Self::new(ERR_NOT_FOUND, message)
347 }
348
349 pub fn invalid_args(message: impl Into<String>) -> Self {
350 Self::new(ERR_INVALID_ARGS, message)
351 }
352
353 pub fn internal(message: impl Into<String>) -> Self {
354 Self::new(ERR_INTERNAL, message)
355 }
356
357 pub fn timeout(message: impl Into<String>) -> Self {
358 Self::new(ERR_TIMEOUT, message)
359 }
360
361 pub fn capability_denied(message: impl Into<String>) -> Self {
362 Self::new(ERR_CAPABILITY_DENIED, message)
363 }
364}
365
366impl std::fmt::Display for ActError {
367 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
368 write!(f, "{}: {}", self.kind, self.message)
369 }
370}
371
372impl std::error::Error for ActError {}
373
374pub type ActResult<T> = Result<T, ActError>;
376
377#[cfg(test)]
378mod tests {
379 use super::*;
380 use serde_json::json;
381
382 #[test]
383 fn localized_string_plain() {
384 let ls = LocalizedString::plain("hello");
385 assert_eq!(ls.resolve("en"), "hello");
386 assert_eq!(ls.any_text(), "hello");
387 }
388
389 #[test]
390 fn localized_string_from_str() {
391 let ls = LocalizedString::from("hello");
392 assert_eq!(ls.any_text(), "hello");
393 }
394
395 #[test]
396 fn localized_string_default() {
397 let ls = LocalizedString::default();
398 assert_eq!(ls.any_text(), "");
399 }
400
401 #[test]
402 fn localized_string_resolve_by_lang() {
403 let mut map = std::collections::HashMap::new();
404 map.insert("en".to_string(), "hello".to_string());
405 map.insert("ru".to_string(), "привет".to_string());
406 let ls = LocalizedString::Localized(map);
407 assert_eq!(ls.resolve("ru"), "привет");
408 assert_eq!(ls.resolve("en"), "hello");
409 assert!(!ls.resolve("fr").is_empty());
411 }
412
413 #[test]
414 fn localized_string_resolve_prefix() {
415 let mut map = HashMap::new();
416 map.insert("zh-Hans".to_string(), "你好".to_string());
417 map.insert("en".to_string(), "hello".to_string());
418 let ls = LocalizedString::Localized(map);
419 assert_eq!(ls.resolve("zh"), "你好");
420 }
421
422 #[test]
423 fn localized_string_get() {
424 let ls = LocalizedString::new("en", "hello");
425 assert_eq!(ls.get("en"), Some("hello"));
426 assert_eq!(ls.get("ru"), None);
427 }
428
429 #[test]
430 fn localized_string_from_vec() {
431 let v = vec![("en".to_string(), "hi".to_string())];
432 let ls = LocalizedString::from(v);
433 assert_eq!(ls.resolve("en"), "hi");
434 }
435
436 #[test]
437 fn metadata_insert_and_get() {
438 let mut m = Metadata::new();
439 m.insert("std:read-only", true);
440 assert_eq!(m.get("std:read-only"), Some(&json!(true)));
441 assert_eq!(m.get_as::<bool>("std:read-only"), Some(true));
442 }
443
444 #[test]
445 fn metadata_to_json_empty() {
446 let json: serde_json::Value = Metadata::new().into();
447 assert_eq!(json, json!({}));
448 }
449
450 #[test]
451 fn metadata_to_json_with_values() {
452 let mut m = Metadata::new();
453 m.insert("std:read-only", true);
454 let json: serde_json::Value = m.into();
455 assert_eq!(json["std:read-only"], json!(true));
456 }
457
458 #[test]
459 fn metadata_from_vec() {
460 let v = vec![("key".to_string(), cbor::to_cbor(&42u32))];
461 let m = Metadata::from(v);
462 assert_eq!(m.get("key"), Some(&json!(42)));
463 assert_eq!(m.get_as::<u32>("key"), Some(42));
464 }
465
466 #[test]
467 fn capabilities_cbor_roundtrip() {
468 let mut info = ComponentInfo::new("test", "0.1.0", "test component");
469 info.capabilities.http = Some(HttpCap {});
470 info.capabilities.filesystem = Some(FilesystemCap {
471 mount_root: Some("/data".to_string()),
472 });
473
474 let mut buf = Vec::new();
475 ciborium::into_writer(&info, &mut buf).unwrap();
476
477 let decoded: ComponentInfo = ciborium::from_reader(&buf[..]).unwrap();
478 assert!(decoded.capabilities.http.is_some());
479 assert!(decoded.capabilities.filesystem.is_some());
480 assert!(decoded.capabilities.sockets.is_none());
481 assert_eq!(decoded.capabilities.fs_mount_root(), Some("/data"));
482 }
483
484 #[test]
485 fn capabilities_empty_roundtrip() {
486 let info = ComponentInfo::new("test", "0.1.0", "test");
487
488 let mut buf = Vec::new();
489 ciborium::into_writer(&info, &mut buf).unwrap();
490
491 let decoded: ComponentInfo = ciborium::from_reader(&buf[..]).unwrap();
492 assert!(decoded.capabilities.is_empty());
493 }
494
495 #[test]
496 fn capabilities_fs_no_params_roundtrip() {
497 let mut info = ComponentInfo::new("test", "0.1.0", "test");
498 info.capabilities.filesystem = Some(FilesystemCap::default());
499
500 let mut buf = Vec::new();
501 ciborium::into_writer(&info, &mut buf).unwrap();
502
503 let decoded: ComponentInfo = ciborium::from_reader(&buf[..]).unwrap();
504 assert!(decoded.capabilities.filesystem.is_some());
505 assert_eq!(decoded.capabilities.fs_mount_root(), None);
506 }
507
508 #[test]
509 fn capabilities_unknown_preserved() {
510 let mut info = ComponentInfo::new("test", "0.1.0", "test");
511 info.capabilities
512 .other
513 .insert("acme:gpu".to_string(), json!({"cores": 8}));
514
515 let mut buf = Vec::new();
516 ciborium::into_writer(&info, &mut buf).unwrap();
517
518 let decoded: ComponentInfo = ciborium::from_reader(&buf[..]).unwrap();
519 assert!(decoded.capabilities.has("acme:gpu"));
520 assert_eq!(decoded.capabilities.other["acme:gpu"]["cores"], 8);
521 }
522}