nut_shell/
response.rs

1//! Response types for command execution.
2//!
3//! `Response` represents successful execution with message and formatting flags.
4
5use crate::config::ShellConfig;
6use core::marker::PhantomData;
7
8/// Command execution response with message and formatting flags.
9/// Command failures return `Err(CliError::CommandFailed(msg))`, not `Response`.
10#[derive(Debug, Clone, PartialEq)]
11pub struct Response<C: ShellConfig> {
12    /// Response message (uses C::MAX_RESPONSE buffer size)
13    pub message: heapless::String<256>, // TODO: Use C::MAX_RESPONSE when const generics stabilize
14
15    /// Message is inline (don't echo newline after command input)
16    pub inline_message: bool,
17
18    /// Add newline before message (in response formatter)
19    pub prefix_newline: bool,
20
21    /// Indent output (2 spaces)
22    pub indent_message: bool,
23
24    /// Add newline after message
25    pub postfix_newline: bool,
26
27    /// Display prompt after response
28    pub show_prompt: bool,
29
30    /// Prevent input from being saved to history
31    #[cfg(feature = "history")]
32    pub exclude_from_history: bool,
33
34    /// Phantom data for config type (will be used when const generics stabilize)
35    _phantom: PhantomData<C>,
36}
37
38impl<C: ShellConfig> Response<C> {
39    /// Create success response with default formatting.
40    ///
41    /// Default: include in history, show prompt, add postfix newline.
42    pub fn success(message: &str) -> Self {
43        let mut msg = heapless::String::new();
44        let _ = msg.push_str(message);
45
46        Self {
47            message: msg,
48            inline_message: false,
49            prefix_newline: false,
50            indent_message: false,
51            postfix_newline: true,
52            show_prompt: true,
53            #[cfg(feature = "history")]
54            exclude_from_history: false,
55            _phantom: PhantomData,
56        }
57    }
58
59    /// Create success response that excludes input from history.
60    ///
61    /// Use for commands handling sensitive data (passwords, credentials).
62    #[cfg(feature = "history")]
63    pub fn success_no_history(message: &str) -> Self {
64        let mut response = Self::success(message);
65        response.exclude_from_history = true;
66        response
67    }
68
69    /// Builder method to exclude command from history (chainable).
70    #[cfg(feature = "history")]
71    pub fn without_history(mut self) -> Self {
72        self.exclude_from_history = true;
73        self
74    }
75
76    /// Builder method for inline response (appears on same line as command).
77    pub fn inline(mut self) -> Self {
78        self.inline_message = true;
79        self
80    }
81
82    /// Builder method to add blank line before response.
83    pub fn with_prefix_newline(mut self) -> Self {
84        self.prefix_newline = true;
85        self
86    }
87
88    /// Builder method to indent response (2 spaces per line).
89    pub fn indented(mut self) -> Self {
90        self.indent_message = true;
91        self
92    }
93
94    /// Builder method to suppress newline after response.
95    pub fn without_postfix_newline(mut self) -> Self {
96        self.postfix_newline = false;
97        self
98    }
99
100    /// Builder method to suppress prompt after response.
101    pub fn without_prompt(mut self) -> Self {
102        self.show_prompt = false;
103        self
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use crate::config::DefaultConfig;
111
112    #[test]
113    fn test_success_response() {
114        let response = Response::<DefaultConfig>::success("OK");
115        assert_eq!(response.message.as_str(), "OK");
116        assert!(!response.inline_message);
117        assert!(!response.prefix_newline);
118        assert!(!response.indent_message);
119        assert!(response.postfix_newline);
120        assert!(response.show_prompt);
121
122        #[cfg(feature = "history")]
123        assert!(!response.exclude_from_history);
124    }
125
126    #[test]
127    fn test_response_empty_message() {
128        let response = Response::<DefaultConfig>::success("");
129        assert_eq!(response.message.as_str(), "");
130    }
131
132    #[test]
133    fn test_response_long_message() {
134        // Test that messages within buffer size are preserved
135        let long_msg = "A".repeat(250);
136        let response = Response::<DefaultConfig>::success(&long_msg);
137        assert_eq!(
138            response.message.len(),
139            250,
140            "Message within capacity should be preserved"
141        );
142
143        // Test that messages exceeding buffer capacity fail silently (heapless behavior)
144        // Note: In production, command handlers should ensure responses fit within MAX_RESPONSE
145        let too_long_msg = "B".repeat(300);
146        let response = Response::<DefaultConfig>::success(&too_long_msg);
147        assert_eq!(
148            response.message.len(),
149            0,
150            "Message exceeding capacity results in empty message (heapless limitation)"
151        );
152    }
153
154    #[test]
155    fn test_builder_chaining() {
156        let response = Response::<DefaultConfig>::success("OK")
157            .inline()
158            .with_prefix_newline()
159            .indented()
160            .without_postfix_newline()
161            .without_prompt();
162
163        assert_eq!(response.message.as_str(), "OK");
164        assert!(response.inline_message);
165        assert!(response.prefix_newline);
166        assert!(response.indent_message);
167        assert!(!response.postfix_newline);
168        assert!(!response.show_prompt);
169    }
170
171    #[test]
172    #[cfg(feature = "history")]
173    fn test_response_exclude_from_history_default() {
174        let response = Response::<DefaultConfig>::success("Test");
175        assert!(!response.exclude_from_history);
176    }
177
178    #[test]
179    #[cfg(feature = "history")]
180    fn test_response_success_no_history() {
181        let response = Response::<DefaultConfig>::success_no_history("Sensitive data");
182        assert!(response.exclude_from_history);
183        assert_eq!(response.message.as_str(), "Sensitive data");
184    }
185
186    #[test]
187    #[cfg(feature = "history")]
188    fn test_response_without_history_builder() {
189        let response = Response::<DefaultConfig>::success("Password set").without_history();
190        assert!(response.exclude_from_history);
191    }
192
193    #[test]
194    #[cfg(feature = "history")]
195    fn test_builder_chaining_with_history() {
196        let response = Response::<DefaultConfig>::success("OK")
197            .inline()
198            .with_prefix_newline()
199            .indented()
200            .without_postfix_newline()
201            .without_prompt()
202            .without_history();
203
204        assert_eq!(response.message.as_str(), "OK");
205        assert!(response.inline_message);
206        assert!(response.prefix_newline);
207        assert!(response.indent_message);
208        assert!(!response.postfix_newline);
209        assert!(!response.show_prompt);
210        assert!(response.exclude_from_history);
211    }
212}