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//! # The stability contract
55//!
56//! A plugin can declare the `autumn-web` range it supports and the
57//! experimental surface it leans on by implementing [`Plugin::contract`]. An
58//! incompatible pairing fails loudly when the plugin is registered, naming both
59//! versions; `autumn plugin-check` reports experimental dependence. See
60//! [`plugin_contract`](crate::plugin_contract) for the declared tiers and the
61//! `SemVer` policy behind each one.
62//!
63//! # Duplicate registration
64//!
65//! Registering two plugins that share the same [`Plugin::name`] is a no-op
66//! after the first: the second call emits a `tracing::warn!` and returns the
67//! builder unchanged. The default name is [`std::any::type_name`] of the
68//! plugin struct, so two different instances of the same type collide by
69//! default -- override [`Plugin::name`] if a plugin is genuinely designed to
70//! be registered more than once.
71
72use std::borrow::Cow;
73
74use crate::app::AppBuilder;
75use crate::plugin_contract::PluginContract;
76
77/// A reusable Autumn integration that wires itself into an [`AppBuilder`].
78///
79/// See the [module-level documentation](self) for conventions and examples.
80pub trait Plugin: Sized + Send + 'static {
81    /// Stable identifier used for duplicate-registration detection.
82    ///
83    /// Defaults to [`std::any::type_name`] of the concrete plugin struct, so
84    /// two instances of the same type collide by default. Override to allow
85    /// multiple instances of the same type to coexist; the return type is
86    /// [`Cow<'static, str>`](std::borrow::Cow) so plugins can compute a
87    /// unique label from runtime configuration without leaking memory.
88    fn name(&self) -> Cow<'static, str> {
89        Cow::Borrowed(std::any::type_name::<Self>())
90    }
91
92    /// The route group of the routes this plugin adds (#3544).
93    ///
94    /// Defaults to [`name`](Self::name). A `[roles.<name>] mount = [...]` list
95    /// names groups. Override this method to give the routes a short group
96    /// name, or to put them in an app group such as `"marketing"`. A route
97    /// that names its own group keeps it.
98    fn route_group(&self) -> Cow<'static, str> {
99        self.name()
100    }
101
102    /// Apply this plugin's configuration to the builder.
103    ///
104    /// Called exactly once per `AppBuilder`. Implementations typically chain
105    /// [`AppBuilder::on_startup`], [`AppBuilder::on_shutdown`],
106    /// [`AppBuilder::nest`], [`AppBuilder::with_extension`] and (with the
107    /// `db` feature) [`AppBuilder::migrations`].
108    ///
109    /// Plugins can also install **tier-1 subsystem replacements** here —
110    /// [`AppBuilder::with_config_loader`], [`AppBuilder::with_pool_provider`]
111    /// (with the `db` feature), [`AppBuilder::with_telemetry_provider`], and
112    /// [`AppBuilder::with_session_store`] — which is the canonical way to
113    /// distribute a custom subsystem (e.g. `AwsSecretsConfigPlugin`) for
114    /// downstream consumers as a one-line install. See
115    /// `docs/guide/extensibility.md` for the full extensibility model.
116    #[must_use]
117    fn build(self, app: AppBuilder) -> AppBuilder;
118
119    /// Declare which `autumn-web` versions this plugin supports, and any
120    /// experimental surface it knowingly depends on.
121    ///
122    /// Returning `None` (the default) declares nothing and changes no
123    /// behaviour — every plugin written before the contract existed keeps
124    /// working. Returning a contract whose
125    /// [`autumn_web`](PluginContract::autumn_web) range excludes the framework
126    /// this plugin is compiled into makes
127    /// [`AppBuilder::plugin`](crate::app::AppBuilder::plugin) **panic at
128    /// registration** with a diagnostic naming both versions and both
129    /// remedies. That is deliberate: a silent mismatch is the failure mode
130    /// this contract exists to remove.
131    ///
132    /// See [`plugin_contract`](crate::plugin_contract) for the stability tiers
133    /// and the `SemVer` policy each one carries.
134    ///
135    /// ```rust
136    /// # use autumn_web::app::AppBuilder;
137    /// # use autumn_web::plugin::Plugin;
138    /// use autumn_web::plugin_contract::PluginContract;
139    ///
140    /// # struct MyPlugin;
141    /// impl Plugin for MyPlugin {
142    ///     fn contract(&self) -> Option<PluginContract> {
143    ///         Some(
144    ///             PluginContract::new(env!("CARGO_PKG_NAME"))
145    ///                 .plugin_version(env!("CARGO_PKG_VERSION"))
146    ///                 .autumn_web("0.10"),
147    ///         )
148    ///     }
149    ///
150    ///     fn build(self, app: AppBuilder) -> AppBuilder { app }
151    /// }
152    /// ```
153    fn contract(&self) -> Option<PluginContract> {
154        None
155    }
156}
157
158/// A bundle of plugins that can be applied to an [`AppBuilder`] in one call.
159///
160/// Implemented for every [`Plugin`] and for tuples of up to eight plugins.
161/// Used by [`AppBuilder::plugins`](crate::app::AppBuilder::plugins).
162pub trait Plugins: Sized {
163    /// Apply every plugin in this bundle to the builder, in declaration order.
164    #[must_use]
165    fn apply(self, app: AppBuilder) -> AppBuilder;
166}
167
168impl<P: Plugin> Plugins for P {
169    fn apply(self, app: AppBuilder) -> AppBuilder {
170        app.plugin(self)
171    }
172}
173
174macro_rules! impl_plugins_tuple {
175    ($($idx:tt => $ty:ident),+ $(,)?) => {
176        impl<$($ty: Plugin),+> Plugins for ($($ty,)+) {
177            #[allow(non_snake_case)]
178            fn apply(self, app: AppBuilder) -> AppBuilder {
179                let ($($ty,)+) = self;
180                let app = app;
181                $(let app = app.plugin($ty);)+
182                app
183            }
184        }
185    };
186}
187
188impl_plugins_tuple!(0 => P0);
189impl_plugins_tuple!(0 => P0, 1 => P1);
190impl_plugins_tuple!(0 => P0, 1 => P1, 2 => P2);
191impl_plugins_tuple!(0 => P0, 1 => P1, 2 => P2, 3 => P3);
192impl_plugins_tuple!(0 => P0, 1 => P1, 2 => P2, 3 => P3, 4 => P4);
193impl_plugins_tuple!(0 => P0, 1 => P1, 2 => P2, 3 => P3, 4 => P4, 5 => P5);
194impl_plugins_tuple!(0 => P0, 1 => P1, 2 => P2, 3 => P3, 4 => P4, 5 => P5, 6 => P6);
195impl_plugins_tuple!(
196    0 => P0, 1 => P1, 2 => P2, 3 => P3, 4 => P4, 5 => P5, 6 => P6, 7 => P7
197);
198
199#[cfg(test)]
200mod tests {
201    use std::sync::Arc;
202    use std::sync::Mutex;
203
204    use super::*;
205
206    #[derive(Default)]
207    struct Recorder {
208        events: Arc<Mutex<Vec<&'static str>>>,
209    }
210
211    impl Recorder {
212        fn new() -> Self {
213            Self::default()
214        }
215
216        fn events(&self) -> Vec<&'static str> {
217            self.events
218                .lock()
219                .expect("lock shouldn't be poisoned")
220                .clone()
221        }
222
223        fn push(&self, label: &'static str) {
224            self.events
225                .lock()
226                .expect("lock shouldn't be poisoned")
227                .push(label);
228        }
229    }
230
231    struct RecordingPlugin {
232        label: &'static str,
233        recorder: Arc<Recorder>,
234    }
235
236    impl Plugin for RecordingPlugin {
237        fn name(&self) -> Cow<'static, str> {
238            Cow::Borrowed(self.label)
239        }
240
241        fn build(self, app: AppBuilder) -> AppBuilder {
242            self.recorder.push(self.label);
243            app
244        }
245    }
246
247    struct ColaPlugin {
248        recorder: Arc<Recorder>,
249    }
250
251    impl Plugin for ColaPlugin {
252        fn build(self, app: AppBuilder) -> AppBuilder {
253            self.recorder.push("cola");
254            app
255        }
256    }
257
258    struct PepsiPlugin {
259        recorder: Arc<Recorder>,
260    }
261
262    impl Plugin for PepsiPlugin {
263        fn build(self, app: AppBuilder) -> AppBuilder {
264            self.recorder.push("pepsi");
265            app
266        }
267    }
268
269    #[test]
270    fn single_plugin_builds_once() {
271        let recorder = Arc::new(Recorder::new());
272        let builder = crate::app::app().plugin(RecordingPlugin {
273            label: "only",
274            recorder: recorder.clone(),
275        });
276
277        assert_eq!(recorder.events(), vec!["only"]);
278        assert!(builder.has_plugin("only"));
279    }
280
281    #[test]
282    fn duplicate_named_plugin_is_skipped_with_warning() {
283        let recorder = Arc::new(Recorder::new());
284        let builder = crate::app::app()
285            .plugin(RecordingPlugin {
286                label: "dup",
287                recorder: recorder.clone(),
288            })
289            .plugin(RecordingPlugin {
290                label: "dup",
291                recorder: recorder.clone(),
292            });
293
294        assert_eq!(recorder.events(), vec!["dup"]);
295        assert!(builder.has_plugin("dup"));
296    }
297
298    #[test]
299    fn single_plugin_applied_via_plugins_trait() {
300        let recorder = Arc::new(Recorder::new());
301        let builder = crate::app::app().plugins(RecordingPlugin {
302            label: "single_via_trait",
303            recorder: recorder.clone(),
304        });
305
306        assert_eq!(recorder.events(), vec!["single_via_trait"]);
307        assert!(builder.has_plugin("single_via_trait"));
308    }
309
310    #[test]
311    fn tuple_of_plugins_applies_in_declaration_order() {
312        let recorder = Arc::new(Recorder::new());
313        let _builder = crate::app::app().plugins((
314            ColaPlugin {
315                recorder: recorder.clone(),
316            },
317            PepsiPlugin {
318                recorder: recorder.clone(),
319            },
320        ));
321
322        assert_eq!(recorder.events(), vec!["cola", "pepsi"]);
323    }
324}