Skip to main content

click/
decorators.rs

1//! Decorator-like convenience helpers.
2//!
3//! Python Click exposes a decorator factory `make_pass_decorator()` that creates
4//! decorators to pass a context object (stored on `Context.obj`) into callbacks.
5//!
6//! Rust doesn't have function decorators, so click-rs provides a small adapter
7//! that converts a function `Fn(&T, &Context)` into a standard `CommandCallback`
8//! (`Fn(&Context)`), looking up `T` from the context.
9
10use std::marker::PhantomData;
11
12use crate::command::CommandCallback;
13use crate::context::Context;
14use crate::error::{ClickError, Result};
15
16/// A decorator factory for passing a context object of type `T` to a callback.
17///
18/// Create one with [`make_pass_decorator`], then call [`PassDecorator::decorate`]
19/// to wrap a function that expects `&T`.
20///
21/// This matches the most common Click usage (ensure = false). Unlike Python,
22/// click-rs callbacks receive `&Context` (not `&mut Context`), so this helper
23/// does not support "ensure" semantics that would create and store an object
24/// if it is missing.
25pub struct PassDecorator<T> {
26    _marker: PhantomData<T>,
27}
28
29/// Create a pass-decorator for `T`.
30///
31/// # Example
32///
33/// ```rust
34/// use click::{make_pass_decorator, ContextBuilder};
35///
36/// #[derive(Debug)]
37/// struct AppState {
38///     value: i32,
39/// }
40///
41/// let cb = make_pass_decorator::<AppState>().decorate(|state, _ctx| {
42///     assert_eq!(state.value, 123);
43///     Ok(())
44/// });
45///
46/// let ctx = ContextBuilder::new().obj(AppState { value: 123 }).build();
47/// cb(&ctx).unwrap();
48/// ```
49pub fn make_pass_decorator<T: 'static>() -> PassDecorator<T> {
50    PassDecorator {
51        _marker: PhantomData,
52    }
53}
54
55impl<T: 'static> PassDecorator<T> {
56    /// Wrap a function that expects `&T` into a click-rs [`CommandCallback`].
57    pub fn decorate<F>(self, f: F) -> CommandCallback
58    where
59        F: Fn(&T, &Context) -> Result<()> + Send + Sync + 'static,
60    {
61        Box::new(move |ctx: &Context| {
62            let obj = ctx.find_obj::<T>().ok_or_else(|| {
63                ClickError::usage("Missing context object for make_pass_decorator().")
64            })?;
65            f(obj, ctx)
66        })
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73    use crate::context::ContextBuilder;
74    use std::sync::Arc;
75
76    #[derive(Debug)]
77    struct AppState {
78        value: i32,
79    }
80
81    #[test]
82    fn test_make_pass_decorator_passes_obj() {
83        let cb = make_pass_decorator::<AppState>().decorate(|state, _ctx| {
84            assert_eq!(state.value, 7);
85            Ok(())
86        });
87
88        let ctx = ContextBuilder::new().obj(AppState { value: 7 }).build();
89        cb(&ctx).unwrap();
90    }
91
92    #[test]
93    fn test_make_pass_decorator_finds_obj_from_parent() {
94        let cb = make_pass_decorator::<AppState>().decorate(|state, _ctx| {
95            assert_eq!(state.value, 9);
96            Ok(())
97        });
98
99        let parent = Arc::new(ContextBuilder::new().obj(AppState { value: 9 }).build());
100        let child = ContextBuilder::new().parent(parent).build();
101        cb(&child).unwrap();
102    }
103}