Skip to main content

at_parser_rs/
parser.rs

1/***************************************************************************
2 *
3 * AT Command Parser
4 * Copyright (C) 2026 Antonio Salsi <passy.linux@zresa.it>
5 *
6 * This library is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 2.1 of the License, or (at your option) any later version.
10 *
11 * This library is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 * Lesser General Public License for more details.
15 *
16 * You should have received a copy of the GNU Lesser General Public
17 * License along with this library; if not, see <https://www.gnu.org/licenses/>.
18 *
19 ***************************************************************************/
20 
21use crate::context::AtContext;
22use crate::{AtError, AtResult, Args};
23
24/*
25AT Command Forms:
26- AT+CMD     (execution)
27- AT+CMD?    (query)
28- AT+CMD=?   (test)
29- AT+CMD=... (set with arguments)
30 */
31
32/// Represents the different forms an AT command can take
33enum AtForm<'a> {
34    /// Execute command without parameters (AT+CMD)
35    Exec,
36    /// Query the current state (AT+CMD?)
37    Query,
38    /// Test command availability or get valid ranges (AT+CMD=?)
39    Test,
40    /// Set command with arguments (AT+CMD=args)
41    Set(Args<'a>),
42}
43
44/// The main AT command parser
45///
46/// Generic over `T` which must implement the [`AtContext<SIZE>`](crate::context::AtContext) trait,
47/// and over the const `SIZE` which determines the response buffer size.
48///
49/// # Generic Design
50///
51/// The parser is generic over the command handler type `T` and response size `SIZE` to allow
52/// compile-time type checking when all handlers are of the same type. This provides:
53///
54/// - **Type safety**: Compile-time verification of handler types
55/// - **Zero overhead**: No dynamic dispatch when using concrete types
56/// - **Flexibility**: Can be used with trait objects (`dyn AtContext<SIZE>`) for mixed handler types
57///
58/// # Usage Patterns
59///
60/// ## With trait objects (recommended for mixed types):
61/// ```rust,no_run
62/// # use at_parser_rs::parser::AtParser;
63/// # use at_parser_rs::context::AtContext;
64/// # struct Dummy; impl AtContext<64> for Dummy {}
65/// # let mut echo_handler = Dummy; let mut reset_handler = Dummy;
66/// const SIZE: usize = 64;
67/// let mut parser: AtParser<dyn AtContext<SIZE>, SIZE> = AtParser::new();
68/// let commands: &mut [(&str, &mut dyn AtContext<SIZE>)] = &mut [
69///     ("AT+ECHO", &mut echo_handler),
70///     ("AT+RST", &mut reset_handler),
71/// ];
72/// parser.set_commands(commands);
73/// ```
74///
75/// ## With concrete types (for homogeneous handlers):
76/// ```rust,no_run
77/// # use at_parser_rs::parser::AtParser;
78/// # use at_parser_rs::context::AtContext;
79/// # struct MyHandler; impl AtContext<64> for MyHandler {}
80/// # let mut handler1 = MyHandler; let mut handler2 = MyHandler;
81/// const SIZE: usize = 64;
82/// let mut parser: AtParser<MyHandler, SIZE> = AtParser::new();
83/// let commands: &mut [(&str, &mut MyHandler)] = &mut [
84///     ("AT+CMD1", &mut handler1),
85///     ("AT+CMD2", &mut handler2),
86/// ];
87/// parser.set_commands(commands);
88/// ```
89pub struct AtParser<'a, T, const SIZE: usize>
90where
91    T: AtContext<SIZE> + ?Sized {
92    /// Array of registered commands with their command, AT response prefix, and handler
93    pub commands: &'a mut [(&'static str, &'static str, &'a mut T)],
94}
95
96impl<'a, T, const SIZE: usize> AtParser<'a, T, SIZE>
97where
98    T: AtContext<SIZE> + ?Sized {
99
100    /// Create a new empty parser with no registered commands.
101    ///
102    /// Call [`set_commands`](AtParser::set_commands) before dispatching any
103    /// input with [`execute`](AtParser::execute).
104    ///
105    /// # Example
106    ///
107    /// ```rust,no_run
108    /// # use at_parser_rs::parser::AtParser;
109    /// # use at_parser_rs::context::AtContext;
110    /// # const SIZE: usize = 64;
111    /// # struct MyHandler; impl AtContext<SIZE> for MyHandler {}
112    /// let mut parser: AtParser<MyHandler, SIZE> = AtParser::new();
113    /// // parser has no commands yet; execute() will return Err(UnknownCommand)
114    /// ```
115    pub const fn new() -> Self {
116        Self { commands: & mut [] }
117    }
118
119    /// Register the commands that this parser will dispatch.
120    ///
121    /// The slice maps each AT command to a mutable reference to its handler via
122    /// **3-tuples**: `(at_command, at_response, handler)`.  
123    /// - `at_command` — the string matched verbatim against the input prefix (e.g. `"AT+ECHO"`)
124    /// - `at_response` — prefix forwarded to every handler method (e.g. `"+ECHO: "`)
125    /// - `handler` — mutable reference to the [`AtContext`] implementation
126    ///
127    /// Matching is case-sensitive.
128    ///
129    /// # Arguments
130    ///
131    /// * `commands` — mutable slice of `(&'static str, &'static str, &mut T)` triples
132    ///
133    /// # Example
134    ///
135    /// ```rust,no_run
136    /// # use at_parser_rs::parser::AtParser;
137    /// # use at_parser_rs::context::AtContext;
138    /// # use at_parser_rs::{AtResult, at_response};
139    /// # const SIZE: usize = 64;
140    /// struct PingModule;
141    /// impl AtContext<SIZE> for PingModule {
142    ///     fn exec(&mut self, at_response: &'static str) -> AtResult<'_, SIZE> {
143    ///         Ok(at_response!(SIZE, at_response; "PONG"))
144    ///     }
145    /// }
146    ///
147    /// let mut ping = PingModule;
148    /// let mut parser: AtParser<PingModule, SIZE> = AtParser::new();
149    ///
150    /// let commands: &mut [(&str, &str, &mut PingModule)] = &mut [
151    ///     ("AT+PING", "+PING: ", &mut ping),
152    /// ];
153    /// parser.set_commands(commands);
154    /// ```
155    ///
156    /// Using trait objects to mix different handler types:
157    ///
158    /// ```rust,no_run
159    /// # use at_parser_rs::parser::AtParser;
160    /// # use at_parser_rs::context::AtContext;
161    /// # const SIZE: usize = 64;
162    /// # struct PingModule; impl AtContext<SIZE> for PingModule {}
163    /// # struct EchoModule; impl AtContext<SIZE> for EchoModule {}
164    /// let mut ping = PingModule;
165    /// let mut echo = EchoModule;
166    /// let mut parser: AtParser<dyn AtContext<SIZE>, SIZE> = AtParser::new();
167    ///
168    /// let commands: &mut [(&str, &str, &mut dyn AtContext<SIZE>)] = &mut [
169    ///     ("AT+PING", "+PING: ", &mut ping),
170    ///     ("AT+ECHO", "+ECHO: ", &mut echo),
171    /// ];
172    /// parser.set_commands(commands);
173    /// ```
174    pub fn set_commands(&mut self, commands: &'a mut [(&'static str, &'static str, &'a mut T)]) {
175        self.commands = commands;
176    }
177
178    /// Parse and execute an AT command string.
179    ///
180    /// Leading and trailing whitespace is stripped before parsing.
181    /// The command name is matched against the registered commands; if found,
182    /// the appropriate handler method is called based on the command form
183    /// detected from the suffix.
184    ///
185    /// | Input suffix | Dispatches to |
186    /// |---|---|
187    /// | *(none)* | [`exec`](crate::context::AtContext::exec) |
188    /// | `?` | [`query`](crate::context::AtContext::query) |
189    /// | `=?` | [`test`](crate::context::AtContext::test) |
190    /// | `=<args>` | [`set`](crate::context::AtContext::set) |
191    ///
192    /// # Arguments
193    ///
194    /// * `input` — raw AT command string (e.g. `"AT+CMD?"`, `"AT+CMD=1,2"`)
195    ///
196    /// # Returns
197    ///
198    /// * `Ok(Bytes<SIZE>)` — response buffer returned by the matched handler
199    /// * `Err(AtError::UnknownCommand)` — no handler found for the command name
200    /// * `Err(AtError::NotSupported)` — handler found but the requested form is not implemented
201    /// * `Err(AtError::InvalidArgs)` — handler returned an argument error
202    ///
203    /// # Example
204    ///
205    /// ```rust,no_run
206    /// # use at_parser_rs::parser::AtParser;
207    /// # use at_parser_rs::context::AtContext;
208    /// # use at_parser_rs::{Args, AtResult, AtError, at_response};
209    /// # const SIZE: usize = 64;
210    /// struct EchoModule { enabled: bool }
211    /// impl AtContext<SIZE> for EchoModule {
212    ///     fn query(&mut self, at_response: &'static str) -> AtResult<'_, SIZE> {
213    ///         Ok(at_response!(SIZE, at_response; if self.enabled { 1u8 } else { 0u8 }))
214    ///     }
215    ///     fn set(&mut self, at_response: &'static str, args: Args) -> AtResult<'_, SIZE> {
216    ///         let value = args.get(0).ok_or((at_response, AtError::InvalidArgs))?;
217    ///         match value.as_ref() {
218    ///             "0" => { self.enabled = false; Ok(at_response!(SIZE, at_response; "OK")) }
219    ///             "1" => { self.enabled = true;  Ok(at_response!(SIZE, at_response; "OK")) }
220    ///             _ => Err((at_response, AtError::InvalidArgs)),
221    ///         }
222    ///     }
223    /// }
224    ///
225    /// let mut echo = EchoModule { enabled: false };
226    /// let mut parser: AtParser<EchoModule, SIZE> = AtParser::new();
227    /// let commands: &mut [(&str, &str, &mut EchoModule)] = &mut [
228    ///     ("AT+ECHO", "+ECHO: ", &mut echo)
229    /// ];
230    /// parser.set_commands(commands);
231    ///
232    /// assert!(parser.execute("AT+ECHO=1").is_ok());    // Ok(("+ECHO: ", "OK"))
233    /// assert!(parser.execute("AT+ECHO?").is_ok());     // Ok(("+ECHO: ", "1"))
234    /// assert!(parser.execute("AT+UNKNOWN").is_err());  // Err(("", UnknownCommand))
235    /// assert!(parser.execute("AT+ECHO=9").is_err());   // Err(("+ECHO: ", InvalidArgs))
236    /// ```
237    pub fn execute<'b>(&'b mut self, input: &'b str) -> AtResult<'b, SIZE> {
238        let input = input.trim();
239        let (name, form) = parse(input).map_err(|e| ("", e))?;
240
241        // Find the command handler
242        let (_, at_response, module) = self.commands
243            .iter_mut()
244            .find(|(n, _, _)| *n == name)
245            .ok_or(("", AtError::UnknownCommand))?;
246
247        // Dispatch to the appropriate handler method
248        match form {
249            AtForm::Exec => module.exec(at_response),
250            AtForm::Query => module.query(at_response),
251            AtForm::Test => module.test(at_response),
252            AtForm::Set(args) => module.set(at_response, args),
253        }
254    }
255}
256
257/// Parse an AT command string into its name and form.
258///
259/// Examines the suffix of `input` (after trimming whitespace) to determine
260/// which AT command form was requested, then returns the bare command name
261/// together with the detected [`AtForm`].
262///
263/// | Suffix | Resulting form |
264/// |---|---|
265/// | `=?` | [`AtForm::Test`] |
266/// | `?` | [`AtForm::Query`] |
267/// | `=<args>` | [`AtForm::Set`] with the text after `=` as raw args |
268/// | *(none)* | [`AtForm::Exec`] |
269///
270/// This function never returns an error; every well-formed AT command string
271/// maps to exactly one form.
272///
273/// # Arguments
274///
275/// * `input` — pre-trimmed AT command string (trimming is reapplied internally)
276///
277/// # Returns
278///
279/// `Ok((command_name, form))` where `command_name` is a slice of `input`
280/// with the suffix removed.
281fn parse<'a>(input: &'a str) -> Result<(&'a str, AtForm<'a>), AtError<'a>> {
282    let input = input.trim();
283
284    // Check suffixes to determine command form
285    if let Some(cmd) = input.strip_suffix("=?") {
286        Ok((cmd, AtForm::Test))
287    } else if let Some(cmd) = input.strip_suffix('?') {
288        Ok((cmd, AtForm::Query))
289    } else if let Some((cmd, args)) = input.split_once('=') {
290        Ok((cmd, AtForm::Set(Args { raw: args })))
291    } else {
292        Ok((input, AtForm::Exec))
293    }
294}