Skip to main content

autumn_web/
plugin.rs

1//! Plugin trait for composable Autumn integrations.
2//!
3//! A [`Plugin`] encapsulates configuration and wiring for a reusable piece of
4//! infrastructure (durable workflows, live feeds, telemetry exporters, etc.)
5//! that attaches itself to an [`AppBuilder`]. Users register plugins with
6//! [`AppBuilder::plugin`](crate::app::AppBuilder::plugin) or the tuple-taking
7//! [`AppBuilder::plugins`](crate::app::AppBuilder::plugins); each plugin's
8//! [`build`](Plugin::build) runs exactly once.
9//!
10//! # Naming conventions
11//!
12//! First-party plugin crates are named `autumn-<name>-plugin`. Third-party
13//! crates are named `autumn-plugin-<name>` to keep names unambiguous on
14//! crates.io. Each crate exposes a `<Name>Plugin` struct at its root with a
15//! `::new()` constructor and `#[must_use]` fluent configuration methods.
16//!
17//! # Authoring a plugin
18//!
19//! ```rust,no_run
20//! use autumn_web::app::AppBuilder;
21//! use autumn_web::plugin::Plugin;
22//!
23//! pub struct HelloPlugin {
24//!     greeting: String,
25//! }
26//!
27//! impl HelloPlugin {
28//!     #[must_use]
29//!     pub fn new() -> Self {
30//!         Self { greeting: "hello".to_owned() }
31//!     }
32//!
33//!     #[must_use]
34//!     pub fn greeting(mut self, greeting: impl Into<String>) -> Self {
35//!         self.greeting = greeting.into();
36//!         self
37//!     }
38//! }
39//!
40//! impl Plugin for HelloPlugin {
41//!     fn build(self, app: AppBuilder) -> AppBuilder {
42//!         let greeting = self.greeting;
43//!         app.on_startup(move |_state| {
44//!             let greeting = greeting.clone();
45//!             async move {
46//!                 tracing::info!(%greeting, "hello plugin started");
47//!                 Ok(())
48//!             }
49//!         })
50//!     }
51//! }
52//! ```
53//!
54//! # Duplicate registration
55//!
56//! Registering two plugins that share the same [`Plugin::name`] is a no-op
57//! after the first: the second call emits a `tracing::warn!` and returns the
58//! builder unchanged. The default name is [`std::any::type_name`] of the
59//! plugin struct, so two different instances of the same type collide by
60//! default -- override [`Plugin::name`] if a plugin is genuinely designed to
61//! be registered more than once.
62
63use std::borrow::Cow;
64
65use crate::app::AppBuilder;
66
67/// A reusable Autumn integration that wires itself into an [`AppBuilder`].
68///
69/// See the [module-level documentation](self) for conventions and examples.
70pub trait Plugin: Sized + Send + 'static {
71    /// Stable identifier used for duplicate-registration detection.
72    ///
73    /// Defaults to [`std::any::type_name`] of the concrete plugin struct, so
74    /// two instances of the same type collide by default. Override to allow
75    /// multiple instances of the same type to coexist; the return type is
76    /// [`Cow<'static, str>`](std::borrow::Cow) so plugins can compute a
77    /// unique label from runtime configuration without leaking memory.
78    fn name(&self) -> Cow<'static, str> {
79        Cow::Borrowed(std::any::type_name::<Self>())
80    }
81
82    /// Apply this plugin's configuration to the builder.
83    ///
84    /// Called exactly once per `AppBuilder`. Implementations typically chain
85    /// [`AppBuilder::on_startup`], [`AppBuilder::on_shutdown`],
86    /// [`AppBuilder::nest`], [`AppBuilder::with_extension`] and (with the
87    /// `db` feature) [`AppBuilder::migrations`].
88    ///
89    /// Plugins can also install **tier-1 subsystem replacements** here —
90    /// [`AppBuilder::with_config_loader`], [`AppBuilder::with_pool_provider`]
91    /// (with the `db` feature), [`AppBuilder::with_telemetry_provider`], and
92    /// [`AppBuilder::with_session_store`] — which is the canonical way to
93    /// distribute a custom subsystem (e.g. `AwsSecretsConfigPlugin`) for
94    /// downstream consumers as a one-line install. See
95    /// `docs/guide/extensibility.md` for the full extensibility model.
96    #[must_use]
97    fn build(self, app: AppBuilder) -> AppBuilder;
98}
99
100/// A bundle of plugins that can be applied to an [`AppBuilder`] in one call.
101///
102/// Implemented for every [`Plugin`] and for tuples of up to eight plugins.
103/// Used by [`AppBuilder::plugins`](crate::app::AppBuilder::plugins).
104pub trait Plugins: Sized {
105    /// Apply every plugin in this bundle to the builder, in declaration order.
106    #[must_use]
107    fn apply(self, app: AppBuilder) -> AppBuilder;
108}
109
110impl<P: Plugin> Plugins for P {
111    fn apply(self, app: AppBuilder) -> AppBuilder {
112        app.plugin(self)
113    }
114}
115
116macro_rules! impl_plugins_tuple {
117    ($($idx:tt => $ty:ident),+ $(,)?) => {
118        impl<$($ty: Plugin),+> Plugins for ($($ty,)+) {
119            #[allow(non_snake_case)]
120            fn apply(self, app: AppBuilder) -> AppBuilder {
121                let ($($ty,)+) = self;
122                let app = app;
123                $(let app = app.plugin($ty);)+
124                app
125            }
126        }
127    };
128}
129
130impl_plugins_tuple!(0 => P0);
131impl_plugins_tuple!(0 => P0, 1 => P1);
132impl_plugins_tuple!(0 => P0, 1 => P1, 2 => P2);
133impl_plugins_tuple!(0 => P0, 1 => P1, 2 => P2, 3 => P3);
134impl_plugins_tuple!(0 => P0, 1 => P1, 2 => P2, 3 => P3, 4 => P4);
135impl_plugins_tuple!(0 => P0, 1 => P1, 2 => P2, 3 => P3, 4 => P4, 5 => P5);
136impl_plugins_tuple!(0 => P0, 1 => P1, 2 => P2, 3 => P3, 4 => P4, 5 => P5, 6 => P6);
137impl_plugins_tuple!(
138    0 => P0, 1 => P1, 2 => P2, 3 => P3, 4 => P4, 5 => P5, 6 => P6, 7 => P7
139);
140
141#[cfg(test)]
142mod tests {
143    use std::sync::Arc;
144    use std::sync::Mutex;
145
146    use super::*;
147
148    #[derive(Default)]
149    struct Recorder {
150        events: Arc<Mutex<Vec<&'static str>>>,
151    }
152
153    impl Recorder {
154        fn new() -> Self {
155            Self::default()
156        }
157
158        fn events(&self) -> Vec<&'static str> {
159            self.events
160                .lock()
161                .expect("lock shouldn't be poisoned")
162                .clone()
163        }
164
165        fn push(&self, label: &'static str) {
166            self.events
167                .lock()
168                .expect("lock shouldn't be poisoned")
169                .push(label);
170        }
171    }
172
173    struct RecordingPlugin {
174        label: &'static str,
175        recorder: Arc<Recorder>,
176    }
177
178    impl Plugin for RecordingPlugin {
179        fn name(&self) -> Cow<'static, str> {
180            Cow::Borrowed(self.label)
181        }
182
183        fn build(self, app: AppBuilder) -> AppBuilder {
184            self.recorder.push(self.label);
185            app
186        }
187    }
188
189    struct ColaPlugin {
190        recorder: Arc<Recorder>,
191    }
192
193    impl Plugin for ColaPlugin {
194        fn build(self, app: AppBuilder) -> AppBuilder {
195            self.recorder.push("cola");
196            app
197        }
198    }
199
200    struct PepsiPlugin {
201        recorder: Arc<Recorder>,
202    }
203
204    impl Plugin for PepsiPlugin {
205        fn build(self, app: AppBuilder) -> AppBuilder {
206            self.recorder.push("pepsi");
207            app
208        }
209    }
210
211    #[test]
212    fn single_plugin_builds_once() {
213        let recorder = Arc::new(Recorder::new());
214        let builder = crate::app::app().plugin(RecordingPlugin {
215            label: "only",
216            recorder: recorder.clone(),
217        });
218
219        assert_eq!(recorder.events(), vec!["only"]);
220        assert!(builder.has_plugin("only"));
221    }
222
223    #[test]
224    fn duplicate_named_plugin_is_skipped_with_warning() {
225        let recorder = Arc::new(Recorder::new());
226        let builder = crate::app::app()
227            .plugin(RecordingPlugin {
228                label: "dup",
229                recorder: recorder.clone(),
230            })
231            .plugin(RecordingPlugin {
232                label: "dup",
233                recorder: recorder.clone(),
234            });
235
236        assert_eq!(recorder.events(), vec!["dup"]);
237        assert!(builder.has_plugin("dup"));
238    }
239
240    #[test]
241    fn single_plugin_applied_via_plugins_trait() {
242        let recorder = Arc::new(Recorder::new());
243        let builder = crate::app::app().plugins(RecordingPlugin {
244            label: "single_via_trait",
245            recorder: recorder.clone(),
246        });
247
248        assert_eq!(recorder.events(), vec!["single_via_trait"]);
249        assert!(builder.has_plugin("single_via_trait"));
250    }
251
252    #[test]
253    fn tuple_of_plugins_applies_in_declaration_order() {
254        let recorder = Arc::new(Recorder::new());
255        let _builder = crate::app::app().plugins((
256            ColaPlugin {
257                recorder: recorder.clone(),
258            },
259            PepsiPlugin {
260                recorder: recorder.clone(),
261            },
262        ));
263
264        assert_eq!(recorder.events(), vec!["cola", "pepsi"]);
265    }
266}