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 /// Apply this plugin's configuration to the builder.
93 ///
94 /// Called exactly once per `AppBuilder`. Implementations typically chain
95 /// [`AppBuilder::on_startup`], [`AppBuilder::on_shutdown`],
96 /// [`AppBuilder::nest`], [`AppBuilder::with_extension`] and (with the
97 /// `db` feature) [`AppBuilder::migrations`].
98 ///
99 /// Plugins can also install **tier-1 subsystem replacements** here —
100 /// [`AppBuilder::with_config_loader`], [`AppBuilder::with_pool_provider`]
101 /// (with the `db` feature), [`AppBuilder::with_telemetry_provider`], and
102 /// [`AppBuilder::with_session_store`] — which is the canonical way to
103 /// distribute a custom subsystem (e.g. `AwsSecretsConfigPlugin`) for
104 /// downstream consumers as a one-line install. See
105 /// `docs/guide/extensibility.md` for the full extensibility model.
106 #[must_use]
107 fn build(self, app: AppBuilder) -> AppBuilder;
108
109 /// Declare which `autumn-web` versions this plugin supports, and any
110 /// experimental surface it knowingly depends on.
111 ///
112 /// Returning `None` (the default) declares nothing and changes no
113 /// behaviour — every plugin written before the contract existed keeps
114 /// working. Returning a contract whose
115 /// [`autumn_web`](PluginContract::autumn_web) range excludes the framework
116 /// this plugin is compiled into makes
117 /// [`AppBuilder::plugin`](crate::app::AppBuilder::plugin) **panic at
118 /// registration** with a diagnostic naming both versions and both
119 /// remedies. That is deliberate: a silent mismatch is the failure mode
120 /// this contract exists to remove.
121 ///
122 /// See [`plugin_contract`](crate::plugin_contract) for the stability tiers
123 /// and the `SemVer` policy each one carries.
124 ///
125 /// ```rust
126 /// # use autumn_web::app::AppBuilder;
127 /// # use autumn_web::plugin::Plugin;
128 /// use autumn_web::plugin_contract::PluginContract;
129 ///
130 /// # struct MyPlugin;
131 /// impl Plugin for MyPlugin {
132 /// fn contract(&self) -> Option<PluginContract> {
133 /// Some(
134 /// PluginContract::new(env!("CARGO_PKG_NAME"))
135 /// .plugin_version(env!("CARGO_PKG_VERSION"))
136 /// .autumn_web("0.9"),
137 /// )
138 /// }
139 ///
140 /// fn build(self, app: AppBuilder) -> AppBuilder { app }
141 /// }
142 /// ```
143 fn contract(&self) -> Option<PluginContract> {
144 None
145 }
146}
147
148/// A bundle of plugins that can be applied to an [`AppBuilder`] in one call.
149///
150/// Implemented for every [`Plugin`] and for tuples of up to eight plugins.
151/// Used by [`AppBuilder::plugins`](crate::app::AppBuilder::plugins).
152pub trait Plugins: Sized {
153 /// Apply every plugin in this bundle to the builder, in declaration order.
154 #[must_use]
155 fn apply(self, app: AppBuilder) -> AppBuilder;
156}
157
158impl<P: Plugin> Plugins for P {
159 fn apply(self, app: AppBuilder) -> AppBuilder {
160 app.plugin(self)
161 }
162}
163
164macro_rules! impl_plugins_tuple {
165 ($($idx:tt => $ty:ident),+ $(,)?) => {
166 impl<$($ty: Plugin),+> Plugins for ($($ty,)+) {
167 #[allow(non_snake_case)]
168 fn apply(self, app: AppBuilder) -> AppBuilder {
169 let ($($ty,)+) = self;
170 let app = app;
171 $(let app = app.plugin($ty);)+
172 app
173 }
174 }
175 };
176}
177
178impl_plugins_tuple!(0 => P0);
179impl_plugins_tuple!(0 => P0, 1 => P1);
180impl_plugins_tuple!(0 => P0, 1 => P1, 2 => P2);
181impl_plugins_tuple!(0 => P0, 1 => P1, 2 => P2, 3 => P3);
182impl_plugins_tuple!(0 => P0, 1 => P1, 2 => P2, 3 => P3, 4 => P4);
183impl_plugins_tuple!(0 => P0, 1 => P1, 2 => P2, 3 => P3, 4 => P4, 5 => P5);
184impl_plugins_tuple!(0 => P0, 1 => P1, 2 => P2, 3 => P3, 4 => P4, 5 => P5, 6 => P6);
185impl_plugins_tuple!(
186 0 => P0, 1 => P1, 2 => P2, 3 => P3, 4 => P4, 5 => P5, 6 => P6, 7 => P7
187);
188
189#[cfg(test)]
190mod tests {
191 use std::sync::Arc;
192 use std::sync::Mutex;
193
194 use super::*;
195
196 #[derive(Default)]
197 struct Recorder {
198 events: Arc<Mutex<Vec<&'static str>>>,
199 }
200
201 impl Recorder {
202 fn new() -> Self {
203 Self::default()
204 }
205
206 fn events(&self) -> Vec<&'static str> {
207 self.events
208 .lock()
209 .expect("lock shouldn't be poisoned")
210 .clone()
211 }
212
213 fn push(&self, label: &'static str) {
214 self.events
215 .lock()
216 .expect("lock shouldn't be poisoned")
217 .push(label);
218 }
219 }
220
221 struct RecordingPlugin {
222 label: &'static str,
223 recorder: Arc<Recorder>,
224 }
225
226 impl Plugin for RecordingPlugin {
227 fn name(&self) -> Cow<'static, str> {
228 Cow::Borrowed(self.label)
229 }
230
231 fn build(self, app: AppBuilder) -> AppBuilder {
232 self.recorder.push(self.label);
233 app
234 }
235 }
236
237 struct ColaPlugin {
238 recorder: Arc<Recorder>,
239 }
240
241 impl Plugin for ColaPlugin {
242 fn build(self, app: AppBuilder) -> AppBuilder {
243 self.recorder.push("cola");
244 app
245 }
246 }
247
248 struct PepsiPlugin {
249 recorder: Arc<Recorder>,
250 }
251
252 impl Plugin for PepsiPlugin {
253 fn build(self, app: AppBuilder) -> AppBuilder {
254 self.recorder.push("pepsi");
255 app
256 }
257 }
258
259 #[test]
260 fn single_plugin_builds_once() {
261 let recorder = Arc::new(Recorder::new());
262 let builder = crate::app::app().plugin(RecordingPlugin {
263 label: "only",
264 recorder: recorder.clone(),
265 });
266
267 assert_eq!(recorder.events(), vec!["only"]);
268 assert!(builder.has_plugin("only"));
269 }
270
271 #[test]
272 fn duplicate_named_plugin_is_skipped_with_warning() {
273 let recorder = Arc::new(Recorder::new());
274 let builder = crate::app::app()
275 .plugin(RecordingPlugin {
276 label: "dup",
277 recorder: recorder.clone(),
278 })
279 .plugin(RecordingPlugin {
280 label: "dup",
281 recorder: recorder.clone(),
282 });
283
284 assert_eq!(recorder.events(), vec!["dup"]);
285 assert!(builder.has_plugin("dup"));
286 }
287
288 #[test]
289 fn single_plugin_applied_via_plugins_trait() {
290 let recorder = Arc::new(Recorder::new());
291 let builder = crate::app::app().plugins(RecordingPlugin {
292 label: "single_via_trait",
293 recorder: recorder.clone(),
294 });
295
296 assert_eq!(recorder.events(), vec!["single_via_trait"]);
297 assert!(builder.has_plugin("single_via_trait"));
298 }
299
300 #[test]
301 fn tuple_of_plugins_applies_in_declaration_order() {
302 let recorder = Arc::new(Recorder::new());
303 let _builder = crate::app::app().plugins((
304 ColaPlugin {
305 recorder: recorder.clone(),
306 },
307 PepsiPlugin {
308 recorder: recorder.clone(),
309 },
310 ));
311
312 assert_eq!(recorder.events(), vec!["cola", "pepsi"]);
313 }
314}