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 name and handler
93 pub commands: &'a mut [(&'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 name to a mutable reference to its
122 /// handler. Command names are matched verbatim and case-sensitively
123 /// against the prefix of the input string (before any suffix such as
124 /// `?`, `=?`, or `=<args>`).
125 ///
126 /// # Arguments
127 ///
128 /// * `commands` — mutable slice of `(&'static str, &mut T)` pairs
129 ///
130 /// # Example
131 ///
132 /// ```rust,no_run
133 /// # use at_parser_rs::parser::AtParser;
134 /// # use at_parser_rs::context::AtContext;
135 /// # use at_parser_rs::{AtResult, AtError};
136 /// # use osal_rs::utils::Bytes;
137 /// # const SIZE: usize = 64;
138 /// struct PingModule;
139 /// impl AtContext<SIZE> for PingModule {
140 /// fn exec(&mut self) -> AtResult<'_, SIZE> { Ok(Bytes::from_str("PONG")) }
141 /// }
142 ///
143 /// let mut ping = PingModule;
144 /// let mut parser: AtParser<PingModule, SIZE> = AtParser::new();
145 ///
146 /// let commands: &mut [(&str, &mut PingModule)] = &mut [
147 /// ("AT+PING", &mut ping),
148 /// ];
149 /// parser.set_commands(commands);
150 /// ```
151 ///
152 /// Using trait objects to mix different handler types:
153 ///
154 /// ```rust,no_run
155 /// # use at_parser_rs::parser::AtParser;
156 /// # use at_parser_rs::context::AtContext;
157 /// # use at_parser_rs::{AtResult, AtError};
158 /// # use osal_rs::utils::Bytes;
159 /// # const SIZE: usize = 64;
160 /// # struct PingModule; impl AtContext<SIZE> for PingModule {}
161 /// # struct EchoModule; impl AtContext<SIZE> for EchoModule {}
162 /// let mut ping = PingModule;
163 /// let mut echo = EchoModule;
164 /// let mut parser: AtParser<dyn AtContext<SIZE>, SIZE> = AtParser::new();
165 ///
166 /// let commands: &mut [(&str, &mut dyn AtContext<SIZE>)] = &mut [
167 /// ("AT+PING", &mut ping),
168 /// ("AT+ECHO", &mut echo),
169 /// ];
170 /// parser.set_commands(commands);
171 /// ```
172 pub fn set_commands(&mut self, commands: &'a mut [(&'static str, &'a mut T)]) {
173 self.commands = commands;
174 }
175
176 /// Parse and execute an AT command string.
177 ///
178 /// Leading and trailing whitespace is stripped before parsing.
179 /// The command name is matched against the registered commands; if found,
180 /// the appropriate handler method is called based on the command form
181 /// detected from the suffix.
182 ///
183 /// | Input suffix | Dispatches to |
184 /// |---|---|
185 /// | *(none)* | [`exec`](crate::context::AtContext::exec) |
186 /// | `?` | [`query`](crate::context::AtContext::query) |
187 /// | `=?` | [`test`](crate::context::AtContext::test) |
188 /// | `=<args>` | [`set`](crate::context::AtContext::set) |
189 ///
190 /// # Arguments
191 ///
192 /// * `input` — raw AT command string (e.g. `"AT+CMD?"`, `"AT+CMD=1,2"`)
193 ///
194 /// # Returns
195 ///
196 /// * `Ok(Bytes<SIZE>)` — response buffer returned by the matched handler
197 /// * `Err(AtError::UnknownCommand)` — no handler found for the command name
198 /// * `Err(AtError::NotSupported)` — handler found but the requested form is not implemented
199 /// * `Err(AtError::InvalidArgs)` — handler returned an argument error
200 ///
201 /// # Example
202 ///
203 /// ```rust,no_run
204 /// # use at_parser_rs::parser::AtParser;
205 /// # use at_parser_rs::context::AtContext;
206 /// # use at_parser_rs::{Args, AtResult, AtError};
207 /// # use osal_rs::utils::Bytes;
208 /// # const SIZE: usize = 64;
209 /// struct EchoModule { enabled: bool }
210 /// impl AtContext<SIZE> for EchoModule {
211 /// fn query(&mut self) -> AtResult<'_, SIZE> {
212 /// Ok(Bytes::from_str(if self.enabled { "1" } else { "0" }))
213 /// }
214 /// fn set(&mut self, args: Args) -> AtResult<'_, SIZE> {
215 /// let value = args.get(0).ok_or(AtError::InvalidArgs)?;
216 /// match value.as_ref() {
217 /// "0" => { self.enabled = false; Ok(Bytes::from_str("OK")) }
218 /// "1" => { self.enabled = true; Ok(Bytes::from_str("OK")) }
219 /// _ => Err(AtError::InvalidArgs),
220 /// }
221 /// }
222 /// }
223 ///
224 /// let mut echo = EchoModule { enabled: false };
225 /// let mut parser: AtParser<EchoModule, SIZE> = AtParser::new();
226 /// let commands: &mut [(&str, &mut EchoModule)] = &mut [("AT+ECHO", &mut echo)];
227 /// parser.set_commands(commands);
228 ///
229 /// assert!(parser.execute("AT+ECHO=1").is_ok()); // sets echo on
230 /// assert!(parser.execute("AT+ECHO?").is_ok()); // queries state
231 /// assert!(parser.execute("AT+UNKNOWN").is_err()); // Err(UnknownCommand)
232 /// assert!(parser.execute("AT+ECHO=9").is_err()); // Err(InvalidArgs)
233 /// ```
234 pub fn execute<'b>(&'b mut self, input: &'b str) -> AtResult<'b, SIZE> {
235 let input = input.trim();
236 let (name, form) = parse(input)?;
237
238 // Find the command handler
239 let (_, module) = self.commands
240 .iter_mut()
241 .find(|(n, _)| *n == name)
242 .ok_or(AtError::UnknownCommand)?;
243
244 // Dispatch to the appropriate handler method
245 match form {
246 AtForm::Exec => module.exec(),
247 AtForm::Query => module.query(),
248 AtForm::Test => module.test(),
249 AtForm::Set(args) => module.set(args),
250 }
251 }
252}
253
254/// Parse an AT command string into its name and form.
255///
256/// Examines the suffix of `input` (after trimming whitespace) to determine
257/// which AT command form was requested, then returns the bare command name
258/// together with the detected [`AtForm`].
259///
260/// | Suffix | Resulting form |
261/// |---|---|
262/// | `=?` | [`AtForm::Test`] |
263/// | `?` | [`AtForm::Query`] |
264/// | `=<args>` | [`AtForm::Set`] with the text after `=` as raw args |
265/// | *(none)* | [`AtForm::Exec`] |
266///
267/// This function never returns an error; every well-formed AT command string
268/// maps to exactly one form.
269///
270/// # Arguments
271///
272/// * `input` — pre-trimmed AT command string (trimming is reapplied internally)
273///
274/// # Returns
275///
276/// `Ok((command_name, form))` where `command_name` is a slice of `input`
277/// with the suffix removed.
278fn parse<'a>(input: &'a str) -> Result<(&'a str, AtForm<'a>), AtError<'a>> {
279 let input = input.trim();
280
281 // Check suffixes to determine command form
282 if let Some(cmd) = input.strip_suffix("=?") {
283 Ok((cmd, AtForm::Test))
284 } else if let Some(cmd) = input.strip_suffix('?') {
285 Ok((cmd, AtForm::Query))
286 } else if let Some((cmd, args)) = input.split_once('=') {
287 Ok((cmd, AtForm::Set(Args { raw: args })))
288 } else {
289 Ok((input, AtForm::Exec))
290 }
291}