ashlar_control/
protocol.rs1use std::sync::mpsc::{Receiver, Sender, channel};
8
9use ashlar_command::CommandError;
10use serde_json::{Map, Value, json};
11
12pub const PROTOCOL_VERSION: u32 = 1;
14
15#[derive(Debug, Clone, PartialEq)]
18pub enum Reply {
19 Ok(Value),
20 Err { error: String, detail: Option<Value> },
21}
22
23impl Reply {
24 pub fn ok(v: Value) -> Self {
25 Reply::Ok(v)
26 }
27
28 pub fn err(e: impl std::fmt::Display) -> Self {
29 Reply::Err { error: e.to_string(), detail: None }
30 }
31
32 pub fn err_with(e: impl std::fmt::Display, detail: Value) -> Self {
33 Reply::Err { error: e.to_string(), detail: Some(detail) }
34 }
35
36 pub fn from_command(r: Result<Value, CommandError>) -> Self {
38 match r {
39 Ok(v) => Reply::Ok(v),
40 Err(e) => Reply::Err { error: e.to_string(), detail: serde_json::to_value(&e).ok() },
41 }
42 }
43
44 pub fn is_ok(&self) -> bool {
45 matches!(self, Reply::Ok(_))
46 }
47
48 pub fn to_json(&self, id: Option<&Value>) -> Value {
50 let mut o = Map::new();
51 if let Some(id) = id {
52 o.insert("id".into(), id.clone());
53 }
54 match self {
55 Reply::Ok(v) => {
56 o.insert("ok".into(), Value::Bool(true));
57 o.insert("result".into(), v.clone());
58 }
59 Reply::Err { error, detail } => {
60 o.insert("ok".into(), Value::Bool(false));
61 o.insert("error".into(), Value::String(error.clone()));
62 if let Some(d) = detail {
63 o.insert("detail".into(), d.clone());
64 }
65 }
66 }
67 Value::Object(o)
68 }
69
70 pub fn from_json(v: &Value) -> Option<Self> {
72 match v.get("ok")?.as_bool()? {
73 true => Some(Reply::Ok(v.get("result").cloned().unwrap_or(Value::Null))),
74 false => {
75 Some(Reply::Err { error: v.get("error").and_then(Value::as_str).unwrap_or("error").to_string(), detail: v.get("detail").cloned() })
76 }
77 }
78 }
79
80 pub fn into_result(self) -> Result<Value, (String, Option<Value>)> {
82 match self {
83 Reply::Ok(v) => Ok(v),
84 Reply::Err { error, detail } => Err((error, detail)),
85 }
86 }
87}
88
89#[derive(Debug)]
94pub struct Request {
95 method: String,
96 params: Value,
97 slot: Option<Sender<Reply>>,
98}
99
100impl Request {
101 pub fn new(method: impl Into<String>, params: Value) -> (Self, Receiver<Reply>) {
103 let (tx, rx) = channel();
104 let params = if params.is_null() { json!({}) } else { params };
105 (Self { method: method.into(), params, slot: Some(tx) }, rx)
106 }
107
108 pub fn method(&self) -> &str {
109 &self.method
110 }
111
112 pub fn params(&self) -> &Value {
113 &self.params
114 }
115
116 pub fn reply(mut self, r: Reply) {
118 if let Some(tx) = self.slot.take() {
119 let _ = tx.send(r);
120 }
121 }
122
123 pub fn ok(self, v: Value) {
124 self.reply(Reply::Ok(v));
125 }
126
127 pub fn fail(self, e: impl std::fmt::Display) {
128 self.reply(Reply::err(e));
129 }
130
131 pub fn unknown(self) {
133 let m = self.method.clone();
134 self.fail(format!("unknown method `{m}` (see app.version for the methods this app answers)"));
135 }
136}
137
138impl Drop for Request {
139 fn drop(&mut self) {
140 if let Some(tx) = self.slot.take() {
141 let _ = tx.send(Reply::err(format!("`{}` was dropped by the app without an answer", self.method)));
142 }
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn reply_round_trips() {
152 for r in [Reply::ok(json!({"a": 1})), Reply::err("nope"), Reply::err_with("bad", json!({"steps": []}))] {
153 let wire = r.to_json(Some(&json!(7)));
154 assert_eq!(wire["id"], 7);
155 assert_eq!(Reply::from_json(&wire), Some(r));
156 }
157 assert_eq!(Reply::ok(Value::Null).to_json(None), json!({"ok": true, "result": null}));
158 assert_eq!(Reply::from_json(&json!({"result": 1})), None);
159 assert_eq!(Reply::from_json(&json!({"ok": "yes"})), None);
160 }
161
162 #[test]
163 fn command_errors_keep_their_kind() {
164 let r = Reply::from_command(Err(CommandError::Unknown { id: "x".into() }));
165 let Reply::Err { error, detail } = r else { panic!() };
166 assert_eq!(error, "unknown command `x`");
167 assert_eq!(detail, Some(json!({"kind": "unknown", "id": "x"})));
168 }
169
170 #[test]
171 fn a_dropped_request_answers_itself() {
172 let (req, rx) = Request::new("ui.click", Value::Null);
173 assert_eq!(req.params(), &json!({}));
174 drop(req);
175 let r = rx.try_recv().unwrap();
176 assert!(!r.is_ok());
177 let (req, rx) = Request::new("x", json!({}));
179 req.ok(json!(1));
180 assert_eq!(rx.try_recv().unwrap(), Reply::ok(json!(1)));
181 assert!(rx.try_recv().is_err());
182 }
183}