1use alloc::vec::Vec;
22use core::marker::PhantomData;
23
24use crate::codec;
25use crate::tag::Tag;
26
27pub struct NoCmd;
29
30#[derive(Clone)]
32pub struct Cmd;
33
34#[derive(Clone)]
44#[must_use]
45pub struct CommandBuilder<State> {
46 tag: Tag,
47 buf: Vec<u8>,
48 state: PhantomData<State>,
49}
50
51impl Default for CommandBuilder<NoCmd> {
52 fn default() -> Self {
53 Self::new()
54 }
55}
56
57impl CommandBuilder<NoCmd> {
58 pub fn new() -> Self {
60 Self {
61 tag: Tag::new(),
62 buf: Vec::new(),
63 state: PhantomData,
64 }
65 }
66
67 pub fn with_tag(tag: Tag) -> Self {
74 Self {
75 tag,
76 buf: Vec::new(),
77 state: PhantomData,
78 }
79 }
80
81 pub fn login(username: &str, password: Option<&str>) -> Command {
83 Self::new()
84 .command("/login")
85 .attribute("name", Some(username))
86 .attribute("password", password)
87 .build()
88 }
89
90 pub fn cancel(tag: Tag) -> Command {
92 Self::with_tag(tag)
94 .command("/cancel")
95 .attribute_tag("tag", tag)
96 .build()
97 }
98
99 pub fn command(self, command: &str) -> CommandBuilder<Cmd> {
105 let Self { tag, mut buf, .. } = self;
106
107 codec::encode_word(command.as_bytes(), &mut buf);
109
110 let mut tag_buf = [0u8; 41];
113 tag_buf[..5].copy_from_slice(b".tag=");
114 tag.encode_lower(&mut tag_buf[5..]);
115 codec::encode_word(&tag_buf, &mut buf);
116
117 CommandBuilder {
118 tag,
119 buf,
120 state: PhantomData,
121 }
122 }
123}
124
125impl CommandBuilder<Cmd> {
126 pub fn attribute(self, key: &str, value: Option<&str>) -> Self {
134 let Self { tag, mut buf, .. } = self;
135
136 let value_bytes = value.unwrap_or("");
139 let word_len = 1 + key.len() + 1 + value_bytes.len();
140 codec::encode_length(word_len as u32, &mut buf);
141 buf.push(b'=');
142 buf.extend_from_slice(key.as_bytes());
143 buf.push(b'=');
144 buf.extend_from_slice(value_bytes.as_bytes());
145
146 CommandBuilder {
147 tag,
148 buf,
149 state: PhantomData,
150 }
151 }
152
153 pub fn attribute_raw(self, key: &str, value: Option<&[u8]>) -> Self {
157 let Self { tag, mut buf, .. } = self;
158
159 let value_bytes = value.unwrap_or(&[]);
160 let word_len = 1 + key.len() + 1 + value_bytes.len();
161 codec::encode_length(word_len as u32, &mut buf);
162 buf.push(b'=');
163 buf.extend_from_slice(key.as_bytes());
164 buf.push(b'=');
165 buf.extend_from_slice(value_bytes);
166
167 CommandBuilder {
168 tag,
169 buf,
170 state: PhantomData,
171 }
172 }
173
174 fn attribute_tag(self, key: &str, value: Tag) -> Self {
176 let mut tag_str = [0u8; 36];
177 let s = value.encode_lower(&mut tag_str);
178 self.attribute(key, Some(s))
179 }
180
181 pub fn query_is_present(self, name: &str) -> Self {
185 let Self { tag, mut buf, .. } = self;
186 let word_len = 1 + name.len(); codec::encode_length(word_len as u32, &mut buf);
188 buf.push(b'?');
189 buf.extend_from_slice(name.as_bytes());
190 CommandBuilder {
191 tag,
192 buf,
193 state: PhantomData,
194 }
195 }
196
197 pub fn query_not_present(self, name: &str) -> Self {
199 let Self { tag, mut buf, .. } = self;
200 let word_len = 2 + name.len(); codec::encode_length(word_len as u32, &mut buf);
202 buf.extend_from_slice(b"?-");
203 buf.extend_from_slice(name.as_bytes());
204 CommandBuilder {
205 tag,
206 buf,
207 state: PhantomData,
208 }
209 }
210
211 pub fn query_equal(self, name: &str, value: &str) -> Self {
213 let Self { tag, mut buf, .. } = self;
214 let word_len = 1 + name.len() + 1 + value.len(); codec::encode_length(word_len as u32, &mut buf);
216 buf.push(b'?');
217 buf.extend_from_slice(name.as_bytes());
218 buf.push(b'=');
219 buf.extend_from_slice(value.as_bytes());
220 CommandBuilder {
221 tag,
222 buf,
223 state: PhantomData,
224 }
225 }
226
227 pub fn query_gt(self, key: &str, value: &str) -> Self {
229 let Self { tag, mut buf, .. } = self;
230 let word_len = 2 + key.len() + 1 + value.len(); codec::encode_length(word_len as u32, &mut buf);
232 buf.extend_from_slice(b"?>");
233 buf.extend_from_slice(key.as_bytes());
234 buf.push(b'=');
235 buf.extend_from_slice(value.as_bytes());
236 CommandBuilder {
237 tag,
238 buf,
239 state: PhantomData,
240 }
241 }
242
243 pub fn query_lt(self, key: &str, value: &str) -> Self {
245 let Self { tag, mut buf, .. } = self;
246 let word_len = 2 + key.len() + 1 + value.len(); codec::encode_length(word_len as u32, &mut buf);
248 buf.extend_from_slice(b"?<");
249 buf.extend_from_slice(key.as_bytes());
250 buf.push(b'=');
251 buf.extend_from_slice(value.as_bytes());
252 CommandBuilder {
253 tag,
254 buf,
255 state: PhantomData,
256 }
257 }
258
259 pub fn query_operations(self, operations: impl Iterator<Item = QueryOperator>) -> Self {
263 let Self { tag, mut buf, .. } = self;
264
265 let ops: Vec<u8> = operations.map(QueryOperator::code).collect();
267 let word_len = 2 + ops.len(); codec::encode_length(word_len as u32, &mut buf);
269 buf.extend_from_slice(b"?#");
270 buf.extend_from_slice(&ops);
271
272 CommandBuilder {
273 tag,
274 buf,
275 state: PhantomData,
276 }
277 }
278
279 pub fn build(self) -> Command {
281 let Self { tag, mut buf, .. } = self;
282 codec::encode_terminator(&mut buf);
284 Command { tag, data: buf }
285 }
286}
287
288#[derive(Debug, Clone)]
295pub struct Command {
296 pub tag: Tag,
298 pub(crate) data: Vec<u8>,
300}
301
302impl Command {
303 pub fn data(&self) -> &[u8] {
305 &self.data
306 }
307
308 pub fn into_data(self) -> Vec<u8> {
310 self.data
311 }
312}
313
314#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
316pub enum QueryOperator {
317 Not,
319 And,
321 Or,
323 Dot,
325}
326
327impl QueryOperator {
328 #[inline]
329 fn code(self) -> u8 {
330 match self {
331 QueryOperator::Not => b'!',
332 QueryOperator::And => b'&',
333 QueryOperator::Or => b'|',
334 QueryOperator::Dot => b'.',
335 }
336 }
337}
338
339#[cfg(test)]
340mod tests {
341 use uuid::Uuid;
342
343 use super::*;
344 use alloc::string::String;
345
346 const TEST_TAG: Tag = Tag::from_uuid(Uuid::from_bytes([
347 0xa1, 0xa2, 0xa3, 0xa4, 0xb1, 0xb2, 0xc1, 0xc2, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7,
348 0xd8,
349 ]));
350 const TEST_TAG_WORD: &str = ".tag=a1a2a3a4-b1b2-c1c2-d1d2-d3d4d5d6d7d8";
351
352 fn parse_words(data: &[u8]) -> Vec<String> {
354 let mut words = Vec::new();
355 let mut i = 0;
356 while i < data.len() {
357 let len = data[i] as usize;
358 i += 1;
359 if len == 0 {
360 break;
361 }
362 if i + len > data.len() {
363 panic!("Malformed command data: length prefix exceeds available data.");
364 }
365 let word = &data[i..i + len];
366 i += len;
367 words.push(String::from_utf8_lossy(word).into_owned());
368 }
369 words
370 }
371
372 #[test]
373 fn test_command_builder_new() {
374 let builder = CommandBuilder::<NoCmd>::new();
375 assert_eq!(builder.buf.len(), 0);
376 let a = builder.tag;
378 let b = CommandBuilder::<NoCmd>::new().tag;
379 assert_ne!(a, b);
380 }
381
382 #[test]
383 fn test_command_builder_with_tag() {
384 let builder = CommandBuilder::<NoCmd>::with_tag(TEST_TAG);
385 assert_eq!(builder.tag, TEST_TAG);
386 }
387
388 #[test]
389 fn test_command_builder_command() {
390 let builder = CommandBuilder::<NoCmd>::with_tag(TEST_TAG).command("/interface/print");
391 assert_eq!(builder.buf.len(), 59);
395 assert_eq!(&builder.buf[1..17], b"/interface/print");
396 assert_eq!(&builder.buf[18..59], TEST_TAG_WORD.as_bytes());
397 }
398
399 #[test]
400 fn test_command_builder_attribute() {
401 let builder = CommandBuilder::<NoCmd>::with_tag(TEST_TAG)
402 .command("/interface/print")
403 .attribute("name", Some("ether1"));
404
405 assert_eq!(&builder.buf[60..72], b"=name=ether1");
407 }
408
409 #[test]
410 fn test_command_builder_login() {
411 let command = CommandBuilder::<NoCmd>::login("admin", Some("password"));
412 let s = core::str::from_utf8(&command.data).unwrap();
413 assert!(s.contains("/login"));
414 assert!(s.contains("name=admin"));
415 assert!(s.contains("password=password"));
416 }
417
418 #[test]
419 fn test_command_builder_cancel() {
420 let command = CommandBuilder::<NoCmd>::cancel(TEST_TAG);
421 let s = core::str::from_utf8(&command.data).unwrap();
422 assert!(s.contains("/cancel"));
423 assert!(s.contains("tag=a1a2a3a4-b1b2-c1c2-d1d2-d3d4d5d6d7d8"));
424 }
425
426 #[test]
427 fn test_command_no_attributes() {
428 let cmd = CommandBuilder::new()
429 .command("/system/resource/print")
430 .build();
431 let words = parse_words(&cmd.data);
432
433 assert_eq!(words[0], "/system/resource/print");
434 assert!(words[1].starts_with(".tag="));
435 assert_eq!(words.len(), 2);
436 }
437
438 #[test]
439 fn test_command_with_one_attribute() {
440 let cmd = CommandBuilder::new()
441 .command("/interface/ethernet/print")
442 .attribute("user", Some("admin"))
443 .build();
444 let words = parse_words(&cmd.data);
445
446 assert_eq!(words[0], "/interface/ethernet/print");
447 assert!(words[1].starts_with(".tag="));
448 assert_eq!(words[2], "=user=admin");
449 assert_eq!(words.len(), 3);
450 }
451
452 #[test]
453 fn test_command_with_multiple_attributes() {
454 let cmd = CommandBuilder::new()
455 .command("/some/random")
456 .attribute("attribute_no_value", None)
457 .attribute("another", Some("value"))
458 .build();
459 let words = parse_words(&cmd.data);
460
461 assert_eq!(words[0], "/some/random");
462 assert!(words[1].starts_with(".tag="));
463 assert_eq!(words[2], "=attribute_no_value=");
464 assert_eq!(words[3], "=another=value");
465 assert_eq!(words.len(), 4);
466 }
467
468 #[test]
469 fn test_encode_decode_roundtrip() {
470 let cmd = CommandBuilder::with_tag(TEST_TAG)
472 .command("/interface/print")
473 .attribute("name", Some("ether1"))
474 .attribute("disabled", None)
475 .build();
476
477 match codec::decode_sentence(&cmd.data).unwrap() {
479 codec::Decode::Complete { value: raw, .. } => {
480 let words: Vec<_> = raw.words().collect();
481 assert_eq!(words[0], b"/interface/print");
482 assert_eq!(words[1], TEST_TAG_WORD.as_bytes());
483 assert_eq!(words[2], b"=name=ether1");
484 assert_eq!(words[3], b"=disabled=");
485 assert_eq!(words.len(), 4);
486 }
487 _ => panic!("expected Complete"),
488 }
489 }
490}