Skip to main content

clawless_core/context/
mod.rs

1//! Context for Clawless commands
2//!
3//! This module defines the [`Context`] struct, which provides information about the environment
4//! that Clawless commands are executed in, access to shared resources like loggers, and
5//! configuration settings.
6//!
7//! For information on the context that is available to commands, see the fields and methods of the
8//! [`Context`] struct as well as the types defined in this module.
9
10use bon::bon;
11use getset::Getters;
12
13pub use self::current_working_directory::CurrentWorkingDirectory;
14pub use self::error::ContextError;
15use crate::cancellation::Cancellation;
16use crate::output::Output;
17
18/// Newtype for the directory that a command runs in
19mod current_working_directory;
20/// Errors that occur when Clawless builds a [`Context`]
21mod error;
22
23/// Context for Clawless commands
24///
25/// This struct provides information about the environment that Clawless commands are executed in,
26/// access to shared resources, and configuration settings. It is passed to each command by the
27/// Clawless runtime when executing commands.
28///
29/// # Examples
30///
31/// ```rust,ignore
32/// #[derive(Debug, Args)]
33/// pub struct GreetArgs {
34///     name: String,
35/// }
36///
37/// #[command]
38/// pub async fn greet(args: GreetArgs, context: Context) -> CommandResult {
39///     message!("Hello, {}!", args.name);
40///     Ok(())
41/// }
42/// ```
43// r[impl context.safety.send]
44// r[impl context.safety.sync]
45// r[impl context.safety.unpin]
46#[derive(Clone, Debug, Getters)]
47pub struct Context {
48    /// The working directory in which a command was called
49    // r[impl context.field.cwd]
50    #[getset(get = "pub")]
51    current_working_directory: CurrentWorkingDirectory,
52
53    /// The cancellation token for cooperative shutdown
54    // r[impl cancel.context.field]
55    // r[impl context.field.cancellation]
56    #[getset(get = "pub")]
57    cancellation: Cancellation,
58
59    /// The output handler for sending events to the presenter
60    // r[impl context.field.output]
61    #[getset(get = "pub")]
62    output: Output,
63}
64
65#[bon]
66impl Context {
67    /// Creates a new [`Context`] instance
68    ///
69    /// When `current_working_directory` is omitted, it is auto-detected from the environment.
70    /// When provided explicitly (e.g., in tests), the given value is used directly.
71    ///
72    /// # Errors
73    ///
74    /// Returns [`ContextError::CurrentWorkingDirectory`] if `current_working_directory` is not
75    /// provided and the current working directory cannot be determined from the environment.
76    ///
77    /// # Examples
78    ///
79    /// ```rust,ignore
80    /// // Production: CWD auto-detected
81    /// let context = Context::builder()
82    ///     .output(output)
83    ///     .build()?;
84    ///
85    /// // Tests: explicit CWD
86    /// let context = Context::builder()
87    ///     .current_working_directory(tmp.path())
88    ///     .output(output)
89    ///     .build()?;
90    /// ```
91    // r[impl context.new]
92    // r[impl context.new.error]
93    // r[impl cancel.context.default]
94    // r[impl cancel.context.injectable]
95    #[builder]
96    pub fn new(
97        #[builder(into)] current_working_directory: Option<CurrentWorkingDirectory>,
98        #[builder(default)] cancellation: Cancellation,
99        output: Output,
100    ) -> Result<Self, ContextError> {
101        let current_working_directory = match current_working_directory {
102            Some(cwd) => cwd,
103            None => CurrentWorkingDirectory::try_from_env()?,
104        };
105
106        Ok(Self {
107            current_working_directory,
108            cancellation,
109            output,
110        })
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    // An assertion in a test panics by design. A `# Panics` section on every test
117    // would repeat that and give the reader no information.
118    #![allow(clippy::missing_panics_doc)]
119
120    use std::path::Path;
121
122    use super::*;
123    use crate::event::event_channel;
124
125    fn test_output() -> Output {
126        let (sender, _receiver) = event_channel();
127        Output::new(sender)
128    }
129
130    // r[verify context.field.cancellation]
131    // r[verify cancel.context.injectable]
132    // r[verify cancel.context.field]
133    #[test]
134    fn new_with_cancellation_uses_provided_token() {
135        let cancellation = Cancellation::new();
136        cancellation.cancel();
137
138        let context = Context::builder()
139            .current_working_directory(Path::new("/tmp"))
140            .cancellation(cancellation)
141            .output(test_output())
142            .build()
143            .expect("should create context");
144
145        assert!(context.cancellation().is_cancelled());
146    }
147
148    // r[verify context.field.cwd]
149    #[test]
150    fn new_with_cwd_uses_provided_value() {
151        let context = Context::builder()
152            .current_working_directory(Path::new("/tmp"))
153            .output(test_output())
154            .build()
155            .expect("should create context");
156
157        assert_eq!(context.current_working_directory().get(), Path::new("/tmp"));
158    }
159
160    // r[verify context.new]
161    // r[verify context.field.output]
162    #[test]
163    fn new_with_defaults_detects_cwd() {
164        let expected = std::env::current_dir().expect("should get current dir");
165
166        let context = Context::builder()
167            .output(test_output())
168            .build()
169            .expect("should create context");
170
171        assert_eq!(context.current_working_directory().get(), expected);
172    }
173
174    // r[verify cancel.context.default]
175    #[test]
176    fn new_with_defaults_has_uncancelled_token() {
177        let context = Context::builder()
178            .current_working_directory(Path::new("/tmp"))
179            .output(test_output())
180            .build()
181            .expect("should create context");
182
183        assert!(!context.cancellation().is_cancelled());
184    }
185
186    // r[verify context.safety.send]
187    #[test]
188    fn trait_send() {
189        fn assert_send<T: Send>() {}
190        assert_send::<Context>();
191    }
192
193    // r[verify context.safety.sync]
194    #[test]
195    fn trait_sync() {
196        fn assert_sync<T: Sync>() {}
197        assert_sync::<Context>();
198    }
199
200    // r[verify context.safety.unpin]
201    #[test]
202    fn trait_unpin() {
203        fn assert_unpin<T: Unpin>() {}
204        assert_unpin::<Context>();
205    }
206}