1#![allow(dead_code)]
2use std::collections::HashMap;
14
15use serde::{Deserialize, Serialize};
16
17use super::obj;
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct WebCallType {
25 pub service: String,
26 pub method: String,
27 #[serde(default, skip_serializing_if = "Option::is_none")]
28 pub uicontext: Option<UIContext>,
29 #[serde(default)]
30 pub args: Vec<serde_json::Value>,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct UIContext {
37 #[serde(default, rename = "activetabid")]
38 pub active_tab_id: String,
39 #[serde(flatten)]
40 pub extra: HashMap<String, serde_json::Value>,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct WebReturnType {
47 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
48 pub success: bool,
49 #[serde(default, skip_serializing_if = "Option::is_none")]
50 pub error: Option<String>,
51 #[serde(default, skip_serializing_if = "Option::is_none")]
52 pub data: Option<serde_json::Value>,
53 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub updates: Option<Vec<obj::WaveObjUpdate>>,
55}
56
57impl WebReturnType {
58 pub fn success(data: serde_json::Value) -> Self {
60 Self {
61 success: true,
62 error: None,
63 data: Some(data),
64 updates: None,
65 }
66 }
67
68 pub fn success_empty() -> Self {
70 Self {
71 success: true,
72 error: None,
73 data: None,
74 updates: None,
75 }
76 }
77
78 pub fn success_with_updates(updates: Vec<obj::WaveObjUpdate>) -> Self {
80 Self {
81 success: true,
82 error: None,
83 data: None,
84 updates: if updates.is_empty() {
85 None
86 } else {
87 Some(updates)
88 },
89 }
90 }
91
92 pub fn success_data_updates(
94 data: serde_json::Value,
95 updates: Vec<obj::WaveObjUpdate>,
96 ) -> Self {
97 Self {
98 success: true,
99 error: None,
100 data: Some(data),
101 updates: if updates.is_empty() {
102 None
103 } else {
104 Some(updates)
105 },
106 }
107 }
108
109 pub fn error(msg: impl Into<String>) -> Self {
111 Self {
112 success: false,
113 error: Some(msg.into()),
114 data: None,
115 updates: None,
116 }
117 }
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct MethodMeta {
129 #[serde(default, skip_serializing_if = "Option::is_none")]
130 pub desc: Option<String>,
131 #[serde(default, skip_serializing_if = "Vec::is_empty")]
132 pub arg_names: Vec<String>,
133 #[serde(default, skip_serializing_if = "Option::is_none")]
134 pub return_desc: Option<String>,
135}
136
137pub fn get_method_meta(service: &str, method: &str) -> Option<MethodMeta> {
139 match (service, method) {
140 ("block", "SendCommand") => Some(MethodMeta {
142 desc: Some("send command to block".into()),
143 arg_names: vec!["blockid".into(), "cmd".into()],
144 return_desc: None,
145 }),
146 ("block", "GetControllerStatus") => Some(MethodMeta {
147 desc: Some("get block controller status".into()),
148 arg_names: vec!["blockid".into()],
149 return_desc: None,
150 }),
151 ("block", "SaveTerminalState") => Some(MethodMeta {
152 desc: Some("save the terminal state to a blockfile".into()),
153 arg_names: vec![
154 "ctx".into(),
155 "blockId".into(),
156 "state".into(),
157 "stateType".into(),
158 "ptyOffset".into(),
159 "termSize".into(),
160 ],
161 return_desc: None,
162 }),
163
164 ("object", "GetObject") => Some(MethodMeta {
166 desc: Some("get wave object by oref".into()),
167 arg_names: vec!["oref".into()],
168 return_desc: None,
169 }),
170 ("object", "GetObjects") => Some(MethodMeta {
171 desc: Some("get multiple wave objects".into()),
172 arg_names: vec!["orefs".into()],
173 return_desc: Some("objects".into()),
174 }),
175 ("object", "UpdateTabName") => Some(MethodMeta {
176 desc: Some("update tab name".into()),
177 arg_names: vec!["uiContext".into(), "tabId".into(), "name".into()],
178 return_desc: None,
179 }),
180 ("object", "CreateBlock") => Some(MethodMeta {
181 desc: Some("create a new block".into()),
182 arg_names: vec![
192 "uiContext".into(),
193 "blockDef".into(),
194 "rtOpts".into(),
195 "tabId".into(),
196 ],
197 return_desc: Some("blockId".into()),
198 }),
199 ("object", "DeleteBlock") => Some(MethodMeta {
200 desc: Some("delete a block".into()),
201 arg_names: vec!["uiContext".into(), "blockId".into()],
202 return_desc: None,
203 }),
204 ("object", "UpdateObjectMeta") => Some(MethodMeta {
205 desc: Some("update object meta".into()),
206 arg_names: vec!["uiContext".into(), "oref".into(), "meta".into()],
207 return_desc: None,
208 }),
209 ("object", "UpdateObject") => Some(MethodMeta {
210 desc: Some("update a wave object".into()),
211 arg_names: vec!["uiContext".into(), "waveObj".into(), "returnUpdates".into()],
212 return_desc: None,
213 }),
214
215 ("client", "GetClientData") => Some(MethodMeta {
217 desc: Some("get client data".into()),
218 arg_names: vec![],
219 return_desc: None,
220 }),
221 ("client", "GetTab") => Some(MethodMeta {
222 desc: Some("get tab by ID".into()),
223 arg_names: vec!["tabId".into()],
224 return_desc: None,
225 }),
226 ("client", "GetAllConnStatus") => Some(MethodMeta {
227 desc: Some("get all connection statuses".into()),
228 arg_names: vec![],
229 return_desc: None,
230 }),
231 ("client", "FocusWindow") => Some(MethodMeta {
232 desc: Some("focus a window".into()),
233 arg_names: vec!["windowId".into()],
234 return_desc: None,
235 }),
236 ("client", "AgreeTos") => Some(MethodMeta {
237 desc: Some("agree to terms of service".into()),
238 arg_names: vec![],
239 return_desc: None,
240 }),
241 ("client", "TelemetryUpdate") => Some(MethodMeta {
242 desc: Some("update telemetry setting".into()),
243 arg_names: vec!["telemetryEnabled".into()],
244 return_desc: None,
245 }),
246
247 ("window", "GetWindow") => Some(MethodMeta {
249 desc: Some("get window by ID".into()),
250 arg_names: vec!["windowId".into()],
251 return_desc: None,
252 }),
253 ("window", "CreateWindow") => Some(MethodMeta {
254 desc: Some("create a new window".into()),
255 arg_names: vec!["ctx".into(), "winSize".into(), "workspaceId".into()],
256 return_desc: None,
257 }),
258 ("window", "SetWindowPosAndSize") => Some(MethodMeta {
259 desc: Some("set window position and size".into()),
260 arg_names: vec!["ctx".into(), "windowId".into(), "pos".into(), "size".into()],
261 return_desc: None,
262 }),
263 ("window", "MoveBlockToNewWindow") => Some(MethodMeta {
264 desc: Some("move block to new window".into()),
265 arg_names: vec!["ctx".into(), "currentTabId".into(), "blockId".into()],
266 return_desc: None,
267 }),
268 ("window", "SwitchWorkspace") => Some(MethodMeta {
269 desc: Some("switch workspace".into()),
270 arg_names: vec!["ctx".into(), "windowId".into(), "workspaceId".into()],
271 return_desc: None,
272 }),
273 ("window", "CloseWindow") => Some(MethodMeta {
274 desc: Some("close a window".into()),
275 arg_names: vec!["ctx".into(), "windowId".into(), "fromElectron".into()],
276 return_desc: None,
277 }),
278
279 ("workspace", "CreateWorkspace") => Some(MethodMeta {
281 desc: Some("create a new workspace".into()),
282 arg_names: vec![
283 "ctx".into(),
284 "name".into(),
285 ],
286 return_desc: Some("workspaceId".into()),
287 }),
288 ("workspace", "UpdateWorkspace") => Some(MethodMeta {
289 desc: Some("update workspace properties".into()),
290 arg_names: vec![
291 "ctx".into(),
292 "workspaceId".into(),
293 "name".into(),
294 ],
295 return_desc: None,
296 }),
297 ("workspace", "GetWorkspace") => Some(MethodMeta {
298 desc: Some("get workspace by ID".into()),
299 arg_names: vec!["workspaceId".into()],
300 return_desc: Some("workspace".into()),
301 }),
302 ("workspace", "DeleteWorkspace") => Some(MethodMeta {
303 desc: Some("delete a workspace".into()),
304 arg_names: vec!["workspaceId".into()],
305 return_desc: None,
306 }),
307 ("workspace", "ListWorkspaces") => Some(MethodMeta {
308 desc: Some("list all workspaces".into()),
309 arg_names: vec![],
310 return_desc: None,
311 }),
312 ("workspace", "CreateTab") => Some(MethodMeta {
313 desc: Some("create a new tab".into()),
314 arg_names: vec![
315 "workspaceId".into(),
316 "tabName".into(),
317 "activateTab".into(),
318 "pinned".into(),
319 ],
320 return_desc: Some("tabId".into()),
321 }),
322 ("workspace", "UpdateTabIds") => Some(MethodMeta {
323 desc: Some("update tab ordering".into()),
324 arg_names: vec![
325 "uiContext".into(),
326 "workspaceId".into(),
327 "tabIds".into(),
328 "pinnedTabIds".into(),
329 ],
330 return_desc: None,
331 }),
332 ("workspace", "SetActiveTab") => Some(MethodMeta {
333 desc: Some("set active tab".into()),
334 arg_names: vec!["workspaceId".into(), "tabId".into()],
335 return_desc: None,
336 }),
337 ("workspace", "CloseTab") => Some(MethodMeta {
338 desc: Some("close a tab".into()),
339 arg_names: vec![
340 "ctx".into(),
341 "workspaceId".into(),
342 "tabId".into(),
343 "fromElectron".into(),
344 ],
345 return_desc: Some("CloseTabRtn".into()),
346 }),
347
348 ("userinput", "SendUserInputResponse") => Some(MethodMeta {
350 desc: Some("send user input response".into()),
351 arg_names: vec!["response".into()],
352 return_desc: None,
353 }),
354
355 _ => None,
356 }
357}
358
359#[derive(Debug, Clone, Serialize, Deserialize)]
364pub struct CloseTabRtnType {
365 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
366 pub closewindow: bool,
367 #[serde(default, skip_serializing_if = "String::is_empty")]
368 pub newactivetabid: String,
369}
370
371pub const SERVICES: &[&str] = &[
373 "block",
374 "object",
375 "client",
376 "window",
377 "workspace",
378 "userinput",
379];
380
381pub fn is_valid_service(name: &str) -> bool {
383 SERVICES.contains(&name)
384}
385
386pub fn get_arg<T: serde::de::DeserializeOwned>(
388 args: &[serde_json::Value],
389 idx: usize,
390) -> Result<T, String> {
391 let val = args
392 .get(idx)
393 .ok_or_else(|| format!("missing argument at index {}", idx))?;
394 serde_json::from_value(val.clone()).map_err(|e| format!("invalid argument at index {}: {}", idx, e))
395}
396
397pub fn get_optional_arg<T: serde::de::DeserializeOwned>(
400 args: &[serde_json::Value],
401 idx: usize,
402) -> Result<Option<T>, String> {
403 match args.get(idx) {
404 None | Some(serde_json::Value::Null) => Ok(None),
405 Some(val) => serde_json::from_value(val.clone())
406 .map(Some)
407 .map_err(|e| format!("invalid argument at index {}: {}", idx, e)),
408 }
409}
410
411#[cfg(test)]
412mod tests {
413 use super::*;
414
415 #[test]
416 fn test_web_call_type_deserialize() {
417 let json = r#"{
418 "service": "object",
419 "method": "GetObject",
420 "args": ["block:abc123"]
421 }"#;
422 let call: WebCallType = serde_json::from_str(json).unwrap();
423 assert_eq!(call.service, "object");
424 assert_eq!(call.method, "GetObject");
425 assert_eq!(call.args.len(), 1);
426 assert!(call.uicontext.is_none());
427 }
428
429 #[test]
430 fn test_web_call_type_with_uicontext() {
431 let json = r#"{
432 "service": "object",
433 "method": "CreateBlock",
434 "uicontext": {"activetabid": "tab-123"},
435 "args": [{"view": "term"}, {}]
436 }"#;
437 let call: WebCallType = serde_json::from_str(json).unwrap();
438 assert_eq!(call.service, "object");
439 assert_eq!(call.method, "CreateBlock");
440 assert_eq!(call.uicontext.as_ref().unwrap().active_tab_id, "tab-123");
441 assert_eq!(call.args.len(), 2);
442 }
443
444 #[test]
445 fn test_web_return_type_success() {
446 let rtn = WebReturnType::success(serde_json::json!("hello"));
447 assert!(rtn.success);
448 assert!(rtn.error.is_none());
449 assert_eq!(rtn.data, Some(serde_json::json!("hello")));
450 }
451
452 #[test]
453 fn test_web_return_type_error() {
454 let rtn = WebReturnType::error("something went wrong");
455 assert!(!rtn.success);
456 assert_eq!(rtn.error.as_deref(), Some("something went wrong"));
457 assert!(rtn.data.is_none());
458 }
459
460 #[test]
461 fn test_web_return_type_success_empty() {
462 let rtn = WebReturnType::success_empty();
463 assert!(rtn.success);
464 assert!(rtn.data.is_none());
465 assert!(rtn.updates.is_none());
466 }
467
468 #[test]
469 fn test_web_return_type_with_updates() {
470 let updates = vec![obj::WaveObjUpdate {
471 updatetype: "update".into(),
472 otype: "tab".into(),
473 oid: "123".into(),
474 obj: None,
475 }];
476 let rtn = WebReturnType::success_with_updates(updates);
477 assert!(rtn.success);
478 assert_eq!(rtn.updates.as_ref().unwrap().len(), 1);
479 }
480
481 #[test]
482 fn test_web_return_type_empty_updates_omitted() {
483 let rtn = WebReturnType::success_with_updates(vec![]);
484 let json = serde_json::to_string(&rtn).unwrap();
485 assert!(!json.contains("updates"));
486 }
487
488 #[test]
489 fn test_web_return_type_serde_roundtrip() {
490 let rtn = WebReturnType::success_data_updates(
491 serde_json::json!({"id": "block-1"}),
492 vec![obj::WaveObjUpdate {
493 updatetype: "update".into(),
494 otype: "block".into(),
495 oid: "abc".into(),
496 obj: Some(serde_json::json!({"otype": "block"})),
497 }],
498 );
499 let json = serde_json::to_string(&rtn).unwrap();
500 let parsed: WebReturnType = serde_json::from_str(&json).unwrap();
501 assert!(parsed.success);
502 assert!(parsed.data.is_some());
503 assert_eq!(parsed.updates.as_ref().unwrap().len(), 1);
504 }
505
506 #[test]
507 fn test_close_tab_rtn_type() {
508 let rtn = CloseTabRtnType {
509 closewindow: true,
510 newactivetabid: String::new(),
511 };
512 let json = serde_json::to_string(&rtn).unwrap();
513 assert!(json.contains("closewindow"));
514 assert!(!json.contains("newactivetabid")); }
516
517 #[test]
518 fn test_method_meta_exists() {
519 assert!(get_method_meta("object", "GetObject").is_some());
520 assert!(get_method_meta("workspace", "CreateTab").is_some());
521 assert!(get_method_meta("block", "SaveTerminalState").is_some());
522 assert!(get_method_meta("nonexistent", "Foo").is_none());
523 }
524
525 #[test]
526 fn test_is_valid_service() {
527 assert!(is_valid_service("block"));
528 assert!(is_valid_service("object"));
529 assert!(is_valid_service("workspace"));
530 assert!(!is_valid_service("invalid"));
531 assert!(!is_valid_service(""));
532 }
533
534 #[test]
535 fn test_get_arg() {
536 let args = vec![
537 serde_json::json!("hello"),
538 serde_json::json!(42),
539 serde_json::json!(true),
540 ];
541 assert_eq!(get_arg::<String>(&args, 0).unwrap(), "hello");
542 assert_eq!(get_arg::<i64>(&args, 1).unwrap(), 42);
543 assert_eq!(get_arg::<bool>(&args, 2).unwrap(), true);
544 assert!(get_arg::<String>(&args, 5).is_err());
545 }
546
547 #[test]
548 fn test_get_optional_arg() {
549 let args = vec![
550 serde_json::json!("hello"),
551 serde_json::Value::Null,
552 ];
553 assert_eq!(
554 get_optional_arg::<String>(&args, 0).unwrap(),
555 Some("hello".to_string())
556 );
557 assert_eq!(get_optional_arg::<String>(&args, 1).unwrap(), None);
558 assert_eq!(get_optional_arg::<String>(&args, 5).unwrap(), None);
559 }
560
561 #[test]
562 fn test_ui_context_deserialize() {
563 let json = r#"{"activetabid": "tab-1", "extra_field": true}"#;
564 let ctx: UIContext = serde_json::from_str(json).unwrap();
565 assert_eq!(ctx.active_tab_id, "tab-1");
566 assert_eq!(ctx.extra.get("extra_field"), Some(&serde_json::json!(true)));
567 }
568
569 #[test]
570 fn test_web_call_no_args() {
571 let json = r#"{"service": "client", "method": "GetClientData"}"#;
572 let call: WebCallType = serde_json::from_str(json).unwrap();
573 assert_eq!(call.service, "client");
574 assert!(call.args.is_empty());
575 }
576}