Nushell plugin for interacting with D-Bus
1use nu_protocol::{Span, Value, record};
2use serde::Deserialize;
3use serde_xml_rs::SerdeXml;
4
5macro_rules! list_to_value {
6 ($list:expr, $span:expr) => {
7 Value::list($list.iter().map(|i| i.to_value($span)).collect(), $span)
8 };
9}
10
11#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Default)]
12#[serde(rename_all = "kebab-case")]
13pub struct Node {
14 #[serde(default, rename = "@name")]
15 pub name: Option<String>,
16 #[serde(default, rename = "interface")]
17 pub interfaces: Vec<Interface>,
18 #[serde(default, rename = "node")]
19 pub children: Vec<Node>,
20}
21
22impl Node {
23 pub fn from_xml(xml: &str) -> Result<Node, serde_xml_rs::Error> {
24 dbg!(xml);
25 let config = SerdeXml::new().overlapping_sequences(true);
26 let mut deserializer = serde_xml_rs::de::Deserializer::from_config(config, xml.as_bytes());
27 Node::deserialize(&mut deserializer)
28 }
29
30 #[cfg(test)]
31 pub fn with_name(name: impl Into<String>) -> Node {
32 Node {
33 name: Some(name.into()),
34 interfaces: vec![],
35 children: vec![],
36 }
37 }
38
39 pub fn get_interface(&self, name: &str) -> Option<&Interface> {
40 self.interfaces.iter().find(|i| i.name == name)
41 }
42
43 /// Find a method on an interface on this node, and then generate the signature of the method
44 /// args
45 pub fn get_method_args_signature(&self, interface: &str, method: &str) -> Option<String> {
46 Some(
47 self.get_interface(interface)?
48 .get_method(method)?
49 .in_signature(),
50 )
51 }
52
53 /// Find the signature of a property on an interface on this node
54 pub fn get_property_signature(&self, interface: &str, property: &str) -> Option<&str> {
55 Some(
56 &self
57 .get_interface(interface)?
58 .get_property(property)?
59 .r#type,
60 )
61 }
62
63 /// Represent the node as a nushell [Value]
64 pub fn to_value(&self, span: Span) -> Value {
65 Value::record(
66 record! {
67 "name" => self.name.as_ref().map(|s| Value::string(s, span)).unwrap_or_default(),
68 "interfaces" => list_to_value!(self.interfaces, span),
69 "children" => list_to_value!(self.children, span),
70 },
71 span,
72 )
73 }
74}
75
76#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
77#[serde(rename_all = "kebab-case")]
78pub struct Interface {
79 #[serde(rename = "@name")]
80 pub name: String,
81 #[serde(default, rename = "method")]
82 pub methods: Vec<Method>,
83 #[serde(default, rename = "signal")]
84 pub signals: Vec<Signal>,
85 #[serde(default, rename = "property")]
86 pub properties: Vec<Property>,
87 #[serde(default, rename = "annotation")]
88 pub annotations: Vec<Annotation>,
89}
90
91impl Interface {
92 pub fn get_method(&self, name: &str) -> Option<&Method> {
93 self.methods.iter().find(|m| m.name == name)
94 }
95
96 #[allow(dead_code)]
97 pub fn get_signal(&self, name: &str) -> Option<&Signal> {
98 self.signals.iter().find(|s| s.name == name)
99 }
100
101 pub fn get_property(&self, name: &str) -> Option<&Property> {
102 self.properties.iter().find(|p| p.name == name)
103 }
104
105 /// Represent the interface as a nushell [Value]
106 pub fn to_value(&self, span: Span) -> Value {
107 Value::record(
108 record! {
109 "name" => Value::string(&self.name, span),
110 "methods" => list_to_value!(self.methods, span),
111 "signals" => list_to_value!(self.signals, span),
112 "properties" => list_to_value!(self.properties, span),
113 "annotations" => list_to_value!(self.annotations, span),
114 },
115 span,
116 )
117 }
118}
119
120#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
121#[serde(rename_all = "kebab-case")]
122pub struct Method {
123 #[serde(rename = "@name")]
124 pub name: String,
125 #[serde(default, rename = "arg")]
126 pub args: Vec<MethodArg>,
127 #[serde(default, rename = "annotation")]
128 pub annotations: Vec<Annotation>,
129}
130
131impl Method {
132 /// Get the signature of the method args
133 pub fn in_signature(&self) -> String {
134 self.args
135 .iter()
136 .filter(|arg| arg.direction == Direction::In)
137 .map(|arg| &arg.r#type[..])
138 .collect()
139 }
140
141 #[allow(dead_code)]
142 /// Get the signature of the method result
143 pub fn out_signature(&self) -> String {
144 self.args
145 .iter()
146 .filter(|arg| arg.direction == Direction::Out)
147 .map(|arg| &arg.r#type[..])
148 .collect()
149 }
150
151 /// Represent the method as a nushell [Value]
152 pub fn to_value(&self, span: Span) -> Value {
153 Value::record(
154 record! {
155 "name" => Value::string(&self.name, span),
156 "args" => list_to_value!(self.args, span),
157 "annotations" => list_to_value!(self.annotations, span),
158 },
159 span,
160 )
161 }
162}
163
164#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
165#[serde(rename_all = "kebab-case")]
166pub struct MethodArg {
167 #[serde(default, rename = "@name")]
168 pub name: Option<String>,
169 #[serde(rename = "@type")]
170 pub r#type: String,
171 #[serde(default, rename = "@direction")]
172 pub direction: Direction,
173}
174
175impl MethodArg {
176 #[cfg(test)]
177 pub fn new(
178 name: impl Into<String>,
179 r#type: impl Into<String>,
180 direction: Direction,
181 ) -> MethodArg {
182 MethodArg {
183 name: Some(name.into()),
184 r#type: r#type.into(),
185 direction,
186 }
187 }
188
189 /// Represent the method as a nushell [Value]
190 pub fn to_value(&self, span: Span) -> Value {
191 Value::record(
192 record! {
193 "name" => self.name.as_ref().map(|n| Value::string(n, span)).unwrap_or_default(),
194 "type" => Value::string(&self.r#type, span),
195 "direction" => self.direction.to_value(span),
196 },
197 span,
198 )
199 }
200}
201
202#[derive(Debug, Clone, Copy, Deserialize, Default, PartialEq, Eq)]
203#[serde(rename_all = "kebab-case")]
204pub enum Direction {
205 #[default]
206 In,
207 Out,
208}
209
210impl Direction {
211 /// Represent the direction as a nushell [Value]
212 pub fn to_value(self, span: Span) -> Value {
213 match self {
214 Direction::In => Value::string("in", span),
215 Direction::Out => Value::string("out", span),
216 }
217 }
218}
219
220#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
221#[serde(rename_all = "kebab-case")]
222pub struct Signal {
223 #[serde(rename = "@name")]
224 pub name: String,
225 #[serde(default, rename = "arg")]
226 pub args: Vec<SignalArg>,
227 #[serde(default, rename = "annotation")]
228 pub annotations: Vec<Annotation>,
229}
230
231impl Signal {
232 /// Represent the signal as a nushell [Value]
233 pub fn to_value(&self, span: Span) -> Value {
234 Value::record(
235 record! {
236 "name" => Value::string(&self.name, span),
237 "args" => list_to_value!(self.args, span),
238 "annotations" => list_to_value!(self.annotations, span),
239 },
240 span,
241 )
242 }
243}
244
245#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
246#[serde(rename_all = "kebab-case")]
247pub struct SignalArg {
248 #[serde(default, rename = "@name")]
249 pub name: Option<String>,
250 #[serde(rename = "@type")]
251 pub r#type: String,
252}
253
254impl SignalArg {
255 /// Represent the argument as a nushell [Value]
256 pub fn to_value(&self, span: Span) -> Value {
257 Value::record(
258 record! {
259 "name" => self.name.as_ref().map(|n| Value::string(n, span)).unwrap_or_default(),
260 "type" => Value::string(&self.r#type, span),
261 },
262 span,
263 )
264 }
265}
266
267#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
268#[serde(rename_all = "kebab-case")]
269pub struct Property {
270 #[serde(rename = "@name")]
271 pub name: String,
272 #[serde(rename = "@type")]
273 pub r#type: String,
274 #[serde(rename = "@access")]
275 pub access: Access,
276 #[serde(default, rename = "annotation")]
277 pub annotations: Vec<Annotation>,
278}
279
280impl Property {
281 /// Represent the property as a nushell [Value]
282 pub fn to_value(&self, span: Span) -> Value {
283 Value::record(
284 record! {
285 "name" => Value::string(&self.name, span),
286 "type" => Value::string(&self.r#type, span),
287 "args" => self.access.to_value(span),
288 "annotations" => list_to_value!(self.annotations, span),
289 },
290 span,
291 )
292 }
293}
294
295#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
296#[serde(rename_all = "lowercase")]
297pub enum Access {
298 Read,
299 Write,
300 ReadWrite,
301}
302
303impl Access {
304 /// Represent the access as a nushell [Value]
305 pub fn to_value(&self, span: Span) -> Value {
306 match self {
307 Access::Read => Value::string("read", span),
308 Access::Write => Value::string("write", span),
309 Access::ReadWrite => Value::string("readwrite", span),
310 }
311 }
312}
313
314#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
315#[serde(rename_all = "kebab-case")]
316pub struct Annotation {
317 #[serde(rename = "@name")]
318 pub name: String,
319 #[serde(rename = "@value")]
320 pub value: String,
321}
322
323impl Annotation {
324 #[cfg(test)]
325 pub fn new(name: impl Into<String>, value: impl Into<String>) -> Annotation {
326 Annotation {
327 name: name.into(),
328 value: value.into(),
329 }
330 }
331
332 /// Represent the annotation as a nushell [Value]
333 pub fn to_value(&self, span: Span) -> Value {
334 Value::record(
335 record! {
336 "name" => Value::string(&self.name, span),
337 "value" => Value::string(&self.value, span),
338 },
339 span,
340 )
341 }
342}
343
344#[cfg(test)]
345pub fn test_introspection_doc_rs() -> Node {
346 Node {
347 name: Some("/com/example/sample_object0".into()),
348 interfaces: vec![Interface {
349 name: "com.example.SampleInterface0".into(),
350 methods: vec![
351 Method {
352 name: "Frobate".into(),
353 args: vec![
354 MethodArg::new("foo", "i", Direction::In),
355 MethodArg::new("bar", "as", Direction::In),
356 MethodArg::new("baz", "a{us}", Direction::Out),
357 ],
358 annotations: vec![Annotation::new("org.freedesktop.DBus.Deprecated", "true")],
359 },
360 Method {
361 name: "Bazify".into(),
362 args: vec![
363 MethodArg::new("bar", "(iiu)", Direction::In),
364 MethodArg::new("len", "u", Direction::Out),
365 MethodArg::new("bar", "v", Direction::Out),
366 ],
367 annotations: vec![],
368 },
369 Method {
370 name: "Mogrify".into(),
371 args: vec![MethodArg::new("bar", "(iiav)", Direction::In)],
372 annotations: vec![],
373 },
374 ],
375 signals: vec![Signal {
376 name: "Changed".into(),
377 args: vec![SignalArg {
378 name: Some("new_value".into()),
379 r#type: "b".into(),
380 }],
381 annotations: vec![],
382 }],
383 properties: vec![Property {
384 name: "Bar".into(),
385 r#type: "y".into(),
386 access: Access::ReadWrite,
387 annotations: vec![],
388 }],
389 annotations: vec![],
390 }],
391 children: vec![
392 Node::with_name("child_of_sample_object"),
393 Node::with_name("another_child_of_sample_object"),
394 ],
395 }
396}
397
398#[test]
399pub fn test_parse_introspection_doc() -> Result<(), serde_xml_rs::Error> {
400 let xml = include_str!("test_introspection_doc.xml");
401 let result = Node::from_xml(xml)?;
402 assert_eq!(result, test_introspection_doc_rs());
403 Ok(())
404}
405
406#[test]
407pub fn test_get_method_args_signature() {
408 assert_eq!(
409 test_introspection_doc_rs()
410 .get_method_args_signature("com.example.SampleInterface0", "Frobate"),
411 Some("ias".into())
412 );
413}