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}