microscpi_common/
lib.rs

1use core::iter::Iterator;
2
3/// Represents a part of an SCPI command, such as "STATus" in "STATus:EVENt?".
4///
5/// Each part has both a short form (uppercase letters only) and a long form (complete word).
6/// SCPI allows using either form in commands.
7///
8/// For example, "STATus" can be written as either "STAT" (short form) or "STATUS" (long form).
9#[derive(Debug, Clone, PartialEq)]
10#[cfg_attr(feature = "serde", derive(serde::Serialize))]
11pub struct CommandPart {
12    /// Whether this command part is optional.
13    pub optional: bool,
14    /// The short form of the command part.
15    pub short: String,
16    /// The long form of the command part.
17    pub long: String,
18}
19
20/// Represents a complete SCPI command with all its parts.
21///
22/// An SCPI command consists of multiple parts separated by colons, for example:
23/// "SYSTem:ERRor:NEXT?" is a command with three parts and is a query (ends with '?').
24///
25/// The command can also have optional parts, indicated by square brackets, like:
26/// "[STATus]:EVENt?" where "STATus" is optional.
27#[derive(Debug, Clone)]
28#[cfg_attr(feature = "serde", derive(serde::Serialize))]
29pub struct Command {
30    /// The parts of the command name.
31    pub parts: Vec<CommandPart>,
32    /// Whether the command is a query, i.e. ends with a question mark.
33    query: bool,
34}
35
36/// Represents a specific path through the command tree, with each string
37/// representing either the short or long form of a command part.
38pub type CommandPath = Vec<String>;
39
40impl TryFrom<&str> for Command {
41    type Error = Box<dyn std::error::Error>;
42
43    /// Parses a command string into a Command structure.
44    ///
45    /// # Arguments
46    /// * `value` - The SCPI command string (e.g., "SYSTem:ERRor?" or "[STATus]:EVENt?")
47    ///
48    /// # Returns
49    /// * `Ok(Command)` - Successfully parsed command
50    /// * `Err` - If the command string is invalid
51    fn try_from(mut value: &str) -> Result<Self, Self::Error> {
52        let mut parts = Vec::new();
53        let mut query = false;
54
55        // Check if the command is a query (ends with '?')
56        if let Some(prefix) = value.strip_suffix('?') {
57            value = prefix;
58            query = true;
59        }
60
61        // Process each part of the command (separated by colons)
62        for part in value.split(':').map(str::trim) {
63            if part.is_empty() {
64                continue;
65            }
66
67            // Check if this part is optional (enclosed in square brackets)
68            let (part, optional) = if part.starts_with('[') && part.ends_with(']') {
69                (&part[1..part.len() - 1], true)
70            } else {
71                (part, false)
72            };
73
74            // The short form consists of only the uppercase letters
75            let short = part.chars().filter(|c| !c.is_lowercase()).collect();
76            // The long form is the entire part in uppercase
77            let long = part.to_uppercase();
78
79            parts.push(CommandPart {
80                optional,
81                short,
82                long,
83            });
84        }
85
86        Ok(Command { parts, query })
87    }
88}
89
90impl Command {
91    /// Returns whether this command is a query (ends with a question mark).
92    pub fn is_query(&self) -> bool {
93        self.query
94    }
95
96    /// Returns the canonical (long-form) representation of this command.
97    ///
98    /// This is the complete command with all parts in their long form,
99    /// separated by colons, and with a question mark at the end if it's a query.
100    pub fn canonical_path(&self) -> String {
101        // Build the path using all long forms
102        let path = self.parts.iter().fold(String::new(), |a, b| {
103            if a.is_empty() {
104                b.long.clone()
105            } else {
106                a + ":" + &b.long
107            }
108        });
109
110        if self.query { path + "?" } else { path }
111    }
112
113    /// Generates all valid paths for this command.
114    ///
115    /// Since SCPI commands can have optional parts and each part can be
116    /// specified in either short or long form, this method generates
117    /// all possible valid combinations.
118    ///
119    /// # Returns
120    /// A vector of all valid command paths
121    pub fn paths(&self) -> Vec<CommandPath> {
122        let mut paths: Vec<CommandPath> = vec![vec![]];
123
124        for part in &self.parts {
125            let mut new_paths: Vec<CommandPath> = Vec::new();
126
127            for path in &mut paths {
128                // Add the long form
129                let mut long_path = path.clone();
130                long_path.push(part.long.clone());
131                new_paths.push(long_path);
132
133                // Add the short form if it's different from the long form
134                if part.short != part.long {
135                    let mut short_path = path.clone();
136                    short_path.push(part.short.clone());
137                    new_paths.push(short_path);
138                }
139
140                // If this part is optional, add a path without it
141                if part.optional {
142                    new_paths.push(path.clone());
143                }
144            }
145
146            paths = new_paths;
147        }
148
149        paths
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn test_all_paths() {
159        let cmd = Command::try_from("[STATus]:TIMe?").unwrap();
160        let paths: Vec<CommandPath> = cmd.paths();
161
162        // Ensure we generate all valid path combinations:
163        // 1. With STATUS (long) + TIME (long)
164        assert!(paths.iter().any(|p| p.as_ref() == vec!["STATUS", "TIME"]));
165        // 2. With STATUS (long) + TIM (short)
166        assert!(paths.iter().any(|p| p.as_ref() == vec!["STATUS", "TIM"]));
167        // 3. With STAT (short) + TIME (long)
168        assert!(paths.iter().any(|p| p.as_ref() == vec!["STAT", "TIME"]));
169        // 4. With STAT (short) + TIM (short)
170        assert!(paths.iter().any(|p| p.as_ref() == vec!["STAT", "TIM"]));
171        // 5. With just TIME (long) - since STATUS is optional
172        assert!(paths.iter().any(|p| p.as_ref() == vec!["TIME"]));
173        // 6. With just TIM (short) - since STATUS is optional
174        assert!(paths.iter().any(|p| p.as_ref() == vec!["TIM"]));
175    }
176
177    #[test]
178    fn test_is_query() {
179        let cmd = Command::try_from("SYSTem:ERRor?").unwrap();
180        assert!(cmd.is_query());
181
182        let cmd = Command::try_from("SYSTem:ERRor").unwrap();
183        assert!(!cmd.is_query());
184    }
185
186    #[test]
187    fn test_canonical_path() {
188        let cmd = Command::try_from("[STATus]:TIMe?").unwrap();
189        assert_eq!(cmd.canonical_path(), "STATUS:TIME?");
190
191        let cmd = Command::try_from("SYSTem:ERRor:NEXT").unwrap();
192        assert_eq!(cmd.canonical_path(), "SYSTEM:ERROR:NEXT");
193    }
194}