loco_rs/app.rs
1//! This module contains the core components and traits for building a web
2//! server application.
3#[cfg(feature = "with-db")]
4use {sea_orm::DatabaseConnection, std::path::Path};
5
6use std::{
7 any::{Any, TypeId},
8 net::SocketAddr,
9 sync::Arc,
10};
11
12use async_trait::async_trait;
13use axum::extract::FromRef;
14use axum::Router as AxumRouter;
15use dashmap::DashMap;
16
17use crate::{
18 bgworker::{self, Queue},
19 boot::{shutdown_signal, BootResult, ServeParams, StartMode},
20 cache::{self},
21 config::Config,
22 controller::{
23 middleware::{self, MiddlewareLayer},
24 AppRoutes,
25 },
26 environment::Environment,
27 mailer::EmailSender,
28 storage::{self, Storage},
29 task::Tasks,
30 Result,
31};
32
33/// Type-safe heterogeneous storage for arbitrary application data
34#[derive(Default, Debug)]
35pub struct SharedStore {
36 // Use DashMap for concurrent access with fine-grained locking
37 storage: DashMap<TypeId, Box<dyn Any + Send + Sync>>,
38}
39
40impl SharedStore {
41 /// Insert a value of type T into the shared store
42 ///
43 /// # Example
44 /// ```
45 /// # use loco_rs::app::SharedStore;
46 /// let shared_store = SharedStore::default();
47 ///
48 /// #[derive(Debug)]
49 /// struct TestService {
50 /// name: String,
51 /// value: i32,
52 /// }
53 ///
54 /// let service = TestService {
55 /// name: "test".to_string(),
56 /// value: 100,
57 /// };
58 ///
59 /// shared_store.insert(service);
60 /// assert!(shared_store.contains::<TestService>());
61 /// ```
62 pub fn insert<T: 'static + Send + Sync>(&self, val: T) {
63 self.storage.insert(TypeId::of::<T>(), Box::new(val));
64 }
65
66 /// Remove a value of type T from the shared store
67 ///
68 /// Returns `Some(T)` if the value was present and removed, `None` otherwise.
69 ///
70 /// # Example
71 /// ```
72 /// # use loco_rs::app::SharedStore;
73 /// let shared_store = SharedStore::default();
74 ///
75 /// struct TestService {
76 /// name: String,
77 /// value: i32,
78 /// }
79 ///
80 /// let service = TestService {
81 /// name: "test".to_string(),
82 /// value: 100,
83 /// };
84 ///
85 /// shared_store.insert(service);
86 /// assert!(shared_store.contains::<TestService>());
87 ///
88 /// // Remove and get the value
89 /// let removed_service_opt = shared_store.remove::<TestService>();
90 /// assert!(removed_service_opt.is_some(), "Service should be present");
91 /// // Assert fields individually instead of comparing the whole struct
92 /// if let Some(removed_service) = removed_service_opt {
93 /// assert_eq!(removed_service.name, "test");
94 /// assert_eq!(removed_service.value, 100);
95 /// }
96 /// // Ensure it's gone
97 /// assert!(!shared_store.contains::<TestService>());
98 ///
99 /// // Trying to remove again returns None
100 /// assert!(shared_store.remove::<TestService>().is_none());
101 /// ```
102 #[must_use]
103 pub fn remove<T: 'static + Send + Sync>(&self) -> Option<T> {
104 self.storage
105 .remove(&TypeId::of::<T>())
106 .map(|(_, v)| v) // Extract the Box<dyn Any>
107 .and_then(|any| any.downcast::<T>().ok()) // Downcast to Box<T>
108 .map(|boxed| *boxed) // Dereference the Box<T> to get T
109 }
110
111 /// Get a reference to a value of type T from the shared store.
112 ///
113 /// Returns `None` if the value doesn't exist.
114 /// The reference is valid for as long as the returned `RefGuard` is held.
115 /// If you need to clone the value, you can do so directly from the
116 /// returned reference, or use the `get` method instead.
117 ///
118 /// # Example
119 /// ```
120 /// # use loco_rs::app::SharedStore;
121 /// let shared_store = SharedStore::default();
122 ///
123 /// #[derive(Clone)]
124 /// struct TestService {
125 /// name: String,
126 /// value: i32,
127 /// }
128 ///
129 /// let service = TestService {
130 /// name: "test".to_string(),
131 /// value: 100,
132 /// };
133 ///
134 /// shared_store.insert(service);
135 ///
136 /// // Get a reference to the service
137 /// let service_ref = shared_store.get_ref::<TestService>().expect("Service not found");
138 /// // Access fields directly
139 /// assert_eq!(service_ref.name, "test");
140 /// assert_eq!(service_ref.value, 100);
141 ///
142 /// // Clone if needed (the field itself)
143 /// let name_clone = service_ref.name.clone();
144 /// assert_eq!(name_clone, "test");
145 ///
146 /// // Compute values from the reference
147 /// let name_len = service_ref.name.len();
148 /// assert_eq!(name_len, 4);
149 /// ```
150 #[must_use]
151 pub fn get_ref<T: 'static + Send + Sync>(&self) -> Option<RefGuard<'_, T>> {
152 let type_id = TypeId::of::<T>();
153 self.storage.get(&type_id).map(|r| RefGuard::<T> {
154 inner: r,
155 _phantom: std::marker::PhantomData,
156 })
157 }
158
159 /// Get a clone of a value of type T from the shared store.
160 /// Requires T to implement Clone.
161 ///
162 /// Returns `None` if the value doesn't exist.
163 /// This method clones the stored value.
164 /// If cloning is not desired or T does not implement Clone,
165 /// use `get_ref` instead.
166 ///
167 /// # Example
168 /// ```
169 /// # use loco_rs::app::SharedStore;
170 /// let shared_store = SharedStore::default();
171 ///
172 /// #[derive(Clone)]
173 /// struct TestService {
174 /// name: String,
175 /// value: i32,
176 /// }
177 ///
178 /// let service = TestService {
179 /// name: "test".to_string(),
180 /// value: 100,
181 /// };
182 ///
183 /// shared_store.insert(service);
184 ///
185 /// // Get a clone of the service
186 /// let service_clone_opt = shared_store.get::<TestService>();
187 /// assert!(service_clone_opt.is_some(), "Service not found");
188 /// // Assert fields individually
189 /// if let Some(ref service_clone) = service_clone_opt {
190 /// assert_eq!(service_clone.name, "test");
191 /// assert_eq!(service_clone.value, 100);
192 /// }
193 /// ```
194 #[must_use]
195 pub fn get<T: 'static + Send + Sync + Clone>(&self) -> Option<T> {
196 self.get_ref::<T>().map(|guard| (*guard).clone())
197 }
198
199 /// Check if the shared store contains a value of type T
200 ///
201 /// # Example
202 /// ```
203 /// # use loco_rs::app::SharedStore;
204 /// let shared_store = SharedStore::default();
205 ///
206 /// struct TestService {
207 /// name: String,
208 /// value: i32,
209 /// }
210 ///
211 /// let service = TestService {
212 /// name: "test".to_string(),
213 /// value: 100,
214 /// };
215 ///
216 /// shared_store.insert(service);
217 /// assert!(shared_store.contains::<TestService>());
218 /// assert!(!shared_store.contains::<String>());
219 /// ```
220 #[must_use]
221 pub fn contains<T: 'static + Send + Sync>(&self) -> bool {
222 self.storage.contains_key(&TypeId::of::<T>())
223 }
224}
225
226// A wrapper around DashMap's Ref type that erases the exact type
227// but provides deref to the target type
228pub struct RefGuard<'a, T: 'static + Send + Sync> {
229 inner: dashmap::mapref::one::Ref<'a, TypeId, Box<dyn Any + Send + Sync>>,
230 _phantom: std::marker::PhantomData<&'a T>,
231}
232
233impl<T: 'static + Send + Sync> std::ops::Deref for RefGuard<'_, T> {
234 type Target = T;
235
236 fn deref(&self) -> &Self::Target {
237 // This is safe because we only create a RefGuard for a specific type
238 // after looking it up by its TypeId
239 #[allow(clippy::coerce_container_to_any)]
240 self.inner
241 .value()
242 .downcast_ref::<T>()
243 .expect("Type mismatch in RefGuard")
244 }
245}
246
247/// Represents the application context for a web server.
248///
249/// This struct encapsulates various components and configurations required by
250/// the web server to operate. It is typically used to store and manage shared
251/// resources and settings that are accessible throughout the application's
252/// lifetime.
253#[derive(Clone, FromRef)]
254#[allow(clippy::module_name_repetitions)]
255#[non_exhaustive]
256pub struct AppContext {
257 /// The environment in which the application is running.
258 pub environment: Environment,
259 #[cfg(feature = "with-db")]
260 /// A database connection used by the application.
261 pub db: DatabaseConnection,
262 /// Queue provider
263 pub queue_provider: Option<Arc<bgworker::Queue>>,
264 /// Configuration settings for the application
265 pub config: Config,
266 /// An optional email sender component that can be used to send email.
267 pub mailer: Option<EmailSender>,
268 // An optional storage instance for the application
269 pub storage: Arc<Storage>,
270 // Cache instance for the application
271 pub cache: Arc<cache::Cache>,
272 /// Shared store for arbitrary application data
273 pub shared_store: Arc<SharedStore>,
274}
275
276/// Builder for [`AppContext`].
277///
278/// Because `AppContext` is `#[non_exhaustive]`,
279/// external crates must construct it through this builder (or the framework's
280/// boot path) rather than a struct literal — so new fields added in future
281/// releases are non-breaking. Required components are constructor arguments;
282/// optional components default to no-op providers unless set.
283#[must_use]
284pub struct AppContextBuilder {
285 environment: Environment,
286 #[cfg(feature = "with-db")]
287 db: DatabaseConnection,
288 config: Config,
289 queue_provider: Option<Arc<bgworker::Queue>>,
290 mailer: Option<EmailSender>,
291 storage: Option<Arc<Storage>>,
292 cache: Option<Arc<cache::Cache>>,
293 shared_store: Option<Arc<SharedStore>>,
294}
295
296impl AppContext {
297 /// Start building an [`AppContext`]. (with-db)
298 #[cfg(feature = "with-db")]
299 pub fn builder(
300 environment: Environment,
301 db: DatabaseConnection,
302 config: Config,
303 ) -> AppContextBuilder {
304 AppContextBuilder {
305 environment,
306 db,
307 config,
308 queue_provider: None,
309 mailer: None,
310 storage: None,
311 cache: None,
312 shared_store: None,
313 }
314 }
315
316 /// Start building an [`AppContext`]. (no-db)
317 #[cfg(not(feature = "with-db"))]
318 pub fn builder(environment: Environment, config: Config) -> AppContextBuilder {
319 AppContextBuilder {
320 environment,
321 config,
322 queue_provider: None,
323 mailer: None,
324 storage: None,
325 cache: None,
326 shared_store: None,
327 }
328 }
329}
330
331impl AppContextBuilder {
332 /// Set the background-queue provider (default: none).
333 pub fn queue_provider(mut self, queue_provider: Arc<bgworker::Queue>) -> Self {
334 self.queue_provider = Some(queue_provider);
335 self
336 }
337 /// Set the email sender (default: none).
338 pub fn mailer(mut self, mailer: EmailSender) -> Self {
339 self.mailer = Some(mailer);
340 self
341 }
342 /// Set the storage (default: single null driver).
343 pub fn storage(mut self, storage: Arc<Storage>) -> Self {
344 self.storage = Some(storage);
345 self
346 }
347 /// Set the cache (default: null cache).
348 pub fn cache(mut self, cache: Arc<cache::Cache>) -> Self {
349 self.cache = Some(cache);
350 self
351 }
352 /// Set the shared store (default: empty).
353 pub fn shared_store(mut self, shared_store: Arc<SharedStore>) -> Self {
354 self.shared_store = Some(shared_store);
355 self
356 }
357 /// Finalize the [`AppContext`], filling any unset optional component with a
358 /// no-op default.
359 #[must_use]
360 pub fn build(self) -> AppContext {
361 AppContext {
362 environment: self.environment,
363 #[cfg(feature = "with-db")]
364 db: self.db,
365 queue_provider: self.queue_provider,
366 config: self.config,
367 mailer: self.mailer,
368 storage: self
369 .storage
370 .unwrap_or_else(|| Storage::single(storage::drivers::null::new()).into()),
371 cache: self
372 .cache
373 .unwrap_or_else(|| cache::Cache::new(cache::drivers::null::new()).into()),
374 shared_store: self
375 .shared_store
376 .unwrap_or_else(|| Arc::new(SharedStore::default())),
377 }
378 }
379}
380
381/// A trait that defines hooks for customizing and extending the behavior of a
382/// web server application.
383///
384/// Users of the web server application should implement this trait to customize
385/// the application's routing, worker connections, task registration, and
386/// database actions according to their specific requirements and use cases.
387#[async_trait]
388pub trait Hooks: Send {
389 /// Defines the composite app version
390 #[must_use]
391 fn app_version() -> String {
392 "dev".to_string()
393 }
394 /// Defines the crate name
395 ///
396 /// Example
397 /// ```rust
398 /// fn app_name() -> &'static str {
399 /// env!("CARGO_CRATE_NAME")
400 /// }
401 /// ```
402 fn app_name() -> &'static str;
403
404 /// Initializes and boots the application based on the specified mode and
405 /// environment.
406 ///
407 /// The boot initialization process may vary depending on whether a DB
408 /// migrator is used or not.
409 ///
410 /// # Examples
411 ///
412 /// With DB:
413 /// ```rust,ignore
414 /// async fn boot(mode: StartMode, environment: &Environment, config: Config) -> Result<BootResult> {
415 /// create_app::<Self, Migrator>(mode, environment, config).await
416 /// }
417 /// ````
418 ///
419 /// Without DB:
420 /// ```rust,ignore
421 /// async fn boot(mode: StartMode, environment: &Environment, config: Config) -> Result<BootResult> {
422 /// create_app::<Self>(mode, environment, config).await
423 /// }
424 /// ````
425 ///
426 ///
427 /// # Errors
428 /// Could not boot the application
429 async fn boot(mode: StartMode, environment: &Environment, config: Config)
430 -> Result<BootResult>;
431
432 /// Start serving the Axum web application on the specified address and
433 /// port.
434 ///
435 /// # Returns
436 /// A Result indicating success () or an error if the server fails to start.
437 async fn serve(app: AxumRouter, ctx: &AppContext, serve_params: &ServeParams) -> Result<()> {
438 let listener = tokio::net::TcpListener::bind(&format!(
439 "{}:{}",
440 serve_params.binding, serve_params.port
441 ))
442 .await?;
443
444 let cloned_ctx = ctx.clone();
445 axum::serve(
446 listener,
447 app.into_make_service_with_connect_info::<SocketAddr>(),
448 )
449 .with_graceful_shutdown(async move {
450 shutdown_signal().await;
451 tracing::info!("shutting down...");
452 Self::on_shutdown(&cloned_ctx).await;
453 })
454 .await?;
455
456 Ok(())
457 }
458
459 /// Override and return `Ok(true)` to provide an alternative logging and
460 /// tracing stack of your own.
461 /// When returning `Ok(true)`, Loco will *not* initialize its own logger,
462 /// so you should set up a complete tracing and logging stack.
463 ///
464 /// # Errors
465 /// If fails returns an error
466 fn init_logger(_ctx: &AppContext) -> Result<bool> {
467 Ok(false)
468 }
469
470 /// Loads the configuration settings for the application based on the given environment.
471 ///
472 /// This function is responsible for retrieving the configuration for the application
473 /// based on the current environment.
474 async fn load_config(env: &Environment) -> Result<Config> {
475 env.load()
476 }
477
478 /// Returns the initial Axum router for the application, allowing the user
479 /// to control the construction of the Axum router. This is where a fallback
480 /// handler can be installed before middleware or other routes are added.
481 ///
482 /// # Errors
483 /// Return an [`Result`] when the router could not be created
484 async fn before_routes(_ctx: &AppContext) -> Result<AxumRouter<AppContext>> {
485 Ok(AxumRouter::new())
486 }
487
488 /// Invoke this function after the Loco routers have been constructed. This
489 /// function enables you to configure custom Axum logics, such as layers,
490 /// that are compatible with Axum.
491 ///
492 /// # Errors
493 /// Axum router error
494 async fn after_routes(router: AxumRouter, _ctx: &AppContext) -> Result<AxumRouter> {
495 Ok(router)
496 }
497
498 /// Provide a list of initializers
499 /// An initializer can be used to seamlessly add functionality to your app
500 /// or to initialize some aspects of it.
501 async fn initializers(_ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {
502 Ok(vec![])
503 }
504
505 /// Provide a list of middlewares
506 #[must_use]
507 fn middlewares(ctx: &AppContext) -> Vec<Box<dyn MiddlewareLayer>> {
508 middleware::default_middleware_stack(ctx)
509 }
510
511 /// Calling the function before run the app
512 /// You can now code some custom loading of resources or other things before
513 /// the app runs
514 async fn before_run(_app_context: &AppContext) -> Result<()> {
515 Ok(())
516 }
517
518 /// Defines the application's routing configuration.
519 fn routes(_ctx: &AppContext) -> AppRoutes;
520
521 // Provides the options to change Loco [`AppContext`] after initialization.
522 async fn after_context(ctx: AppContext) -> Result<AppContext> {
523 Ok(ctx)
524 }
525
526 /// Connects custom workers to the application using the provided
527 /// [`Processor`] and [`AppContext`].
528 async fn connect_workers(ctx: &AppContext, queue: &Queue) -> Result<()>;
529
530 /// Registers custom tasks with the provided [`Tasks`] object.
531 fn register_tasks(tasks: &mut Tasks);
532
533 /// Truncates the database as required. Users should implement this
534 /// function. The truncate controlled from the [`crate::config::Database`]
535 /// by changing `dangerously_truncate` to true (default false).
536 /// Truncate can be useful when you want to truncate the database before any
537 /// test.
538 #[cfg(feature = "with-db")]
539 async fn truncate(_ctx: &AppContext) -> Result<()>;
540
541 /// Seeds the database with initial data.
542 #[cfg(feature = "with-db")]
543 async fn seed(_ctx: &AppContext, path: &Path) -> Result<()>;
544
545 /// Dumps database tables to YAML fixtures under `base`, the counterpart to
546 /// [`Hooks::seed`] used by `cargo loco db seed --dump`.
547 ///
548 /// The default implementation dumps every table via schema introspection
549 /// ([`crate::db::dump_tables`]). Override it to dump specific entities with
550 /// the typed, streaming [`crate::db::dump`] for full type fidelity and
551 /// bounded memory:
552 ///
553 /// ```ignore
554 /// async fn dump(ctx: &AppContext, base: &Path) -> Result<()> {
555 /// db::dump::<users::ActiveModel>(&ctx.db, &base.join("users.yaml").to_string_lossy()).await?;
556 /// Ok(())
557 /// }
558 /// ```
559 #[cfg(feature = "with-db")]
560 async fn dump(ctx: &AppContext, base: &Path) -> Result<()> {
561 crate::db::dump_tables(&ctx.db, base, None).await
562 }
563
564 /// Called when the application is shutting down.
565 /// This function allows users to perform any necessary cleanup or final
566 /// actions before the application stops completely.
567 async fn on_shutdown(_ctx: &AppContext) {}
568}
569
570/// An initializer.
571/// Initializers should be kept in `src/initializers/`
572///
573/// Initializers can provide health checks by implementing the `check` method.
574/// These checks will be run during the `cargo loco doctor` command to validate
575/// the initializer's configuration and test its connections.
576#[async_trait]
577// <snip id="initializers-trait">
578pub trait Initializer: Sync + Send {
579 /// The initializer name or identifier
580 fn name(&self) -> String;
581
582 /// Occurs after the app's `before_run`.
583 /// Use this to for one-time initializations, load caches, perform web
584 /// hooks, etc.
585 async fn before_run(&self, _app_context: &AppContext) -> Result<()> {
586 Ok(())
587 }
588
589 /// Occurs after the app's `after_routes`.
590 /// Use this to compose additional functionality and wire it into an Axum
591 /// Router
592 async fn after_routes(&self, router: AxumRouter, _ctx: &AppContext) -> Result<AxumRouter> {
593 Ok(router)
594 }
595
596 /// Perform health checks for this initializer.
597 /// This method is called during the doctor command to validate the initializer's configuration.
598 /// Return `None` if no check is needed, or `Some(Check)` if a check should be performed.
599 async fn check(&self, _app_context: &AppContext) -> Result<Option<crate::doctor::Check>> {
600 Ok(None)
601 }
602}
603// </snip>
604
605#[cfg(test)]
606mod tests {
607 use super::*;
608 use crate::tests_cfg::app::get_app_context;
609
610 struct TestService {
611 name: String,
612 value: i32,
613 }
614
615 #[derive(Clone)]
616 struct CloneableTestService {
617 name: String,
618 value: i32,
619 }
620
621 #[test]
622 fn test_extensions_insert_and_get() {
623 // Setup
624 let shared_store = SharedStore::default();
625
626 shared_store.insert(42i32);
627 assert_eq!(shared_store.get::<i32>().expect("Value should exist"), 42);
628
629 let service = TestService {
630 name: "test".to_string(),
631 value: 100,
632 };
633
634 shared_store.insert(service);
635
636 let service_ref_opt = shared_store.get_ref::<TestService>();
637 assert!(service_ref_opt.is_some(), "Service ref should exist");
638 if let Some(service_ref) = service_ref_opt {
639 assert_eq!(service_ref.name, "test");
640 assert_eq!(service_ref.value, 100);
641 let name_clone = service_ref.name.clone();
642 assert_eq!(name_clone, "test");
643 } else {
644 panic!("Should have gotten Some(service_ref)");
645 }
646 }
647
648 #[test]
649 fn test_extensions_get_without_clone() {
650 let shared_store = SharedStore::default();
651
652 let service = TestService {
653 name: "test_direct".to_string(),
654 value: 100,
655 };
656 shared_store.insert(service);
657
658 let service_ref_opt = shared_store.get_ref::<TestService>();
659 assert!(service_ref_opt.is_some(), "Service ref should exist");
660 if let Some(service_ref) = service_ref_opt {
661 assert_eq!(service_ref.name, "test_direct");
662 assert_eq!(service_ref.value, 100);
663 } else {
664 panic!("Should have gotten Some(service_ref)");
665 }
666
667 let name_len_opt = shared_store.get_ref::<TestService>().map(|r| r.name.len());
668 assert!(
669 name_len_opt.is_some(),
670 "Service ref should exist for len check"
671 );
672 assert_eq!(name_len_opt.unwrap(), 11);
673
674 let value_opt = shared_store.get_ref::<TestService>().map(|r| r.value);
675 assert!(
676 value_opt.is_some(),
677 "Service ref should exist for value check"
678 );
679 assert_eq!(value_opt.unwrap(), 100);
680 }
681
682 #[test]
683 fn test_extensions_remove() {
684 let shared_store = SharedStore::default();
685
686 shared_store.insert(42i32);
687 assert!(shared_store.contains::<i32>());
688 assert_eq!(shared_store.remove::<i32>(), Some(42));
689 assert!(!shared_store.contains::<i32>());
690 assert_eq!(shared_store.remove::<i32>(), None);
691
692 let service = TestService {
693 name: "rem".to_string(),
694 value: 50,
695 };
696 shared_store.insert(service);
697 assert!(shared_store.contains::<TestService>());
698 let removed_opt = shared_store.remove::<TestService>();
699 assert!(removed_opt.is_some());
700 if let Some(removed) = removed_opt {
701 assert_eq!(removed.name, "rem");
702 assert_eq!(removed.value, 50);
703 } else {
704 panic!("Removed option should be Some");
705 }
706 assert!(!shared_store.contains::<TestService>());
707 assert!(shared_store.remove::<TestService>().is_none());
708 }
709
710 #[test]
711 fn test_extensions_contains() {
712 let shared_store = SharedStore::default();
713
714 shared_store.insert(42i32);
715 shared_store.insert(TestService {
716 name: "contains".to_string(),
717 value: 1,
718 });
719
720 assert!(shared_store.contains::<i32>());
721 assert!(shared_store.contains::<TestService>());
722 assert!(!shared_store.contains::<String>());
723 assert!(!shared_store.contains::<CloneableTestService>());
724 }
725
726 #[test]
727 fn test_extensions_get_cloned() {
728 let shared_store = SharedStore::default();
729
730 shared_store.insert(42i32);
731 assert_eq!(shared_store.get::<i32>(), Some(42));
732 assert!(shared_store.contains::<i32>());
733
734 let service = CloneableTestService {
735 name: "cloned_test".to_string(),
736 value: 200,
737 };
738 shared_store.insert(service.clone());
739
740 let service_clone_opt = shared_store.get::<CloneableTestService>();
741 assert!(service_clone_opt.is_some(), "Cloned service should exist");
742 if let Some(ref service_clone) = service_clone_opt {
743 assert_eq!(service_clone.name, "cloned_test");
744 assert_eq!(service_clone.value, 200);
745 } else {
746 panic!("Should have gotten Some(service_clone)");
747 }
748
749 assert!(shared_store.contains::<CloneableTestService>());
750 let original_ref_opt = shared_store.get_ref::<CloneableTestService>();
751 assert!(original_ref_opt.is_some(), "Original ref should exist");
752 if let Some(original_ref) = original_ref_opt {
753 assert_eq!(original_ref.name, "cloned_test");
754 assert_eq!(original_ref.value, 200);
755 } else {
756 panic!("Should have gotten Some(original_ref)");
757 }
758
759 assert_eq!(shared_store.get::<String>(), None);
760 assert!(shared_store.get::<CloneableTestService>().is_some());
761 // The following line correctly fails to compile because TestService doesn't impl Clone,
762 // which is required by the `get` method.
763 // let non_existent_clone = shared_store.get::<TestService>();
764 }
765
766 #[tokio::test]
767 async fn test_app_context_extensions() {
768 let ctx = get_app_context().await;
769
770 let service_cloneable = CloneableTestService {
771 name: "app_context_test_cloneable".to_string(),
772 value: 42,
773 };
774 ctx.shared_store.insert(service_cloneable.clone());
775
776 let ref_opt = ctx.shared_store.get_ref::<CloneableTestService>();
777 assert!(ref_opt.is_some(), "Cloneable service ref should exist");
778 if let Some(service_ref) = ref_opt {
779 assert_eq!(service_ref.name, "app_context_test_cloneable");
780 assert_eq!(service_ref.value, 42);
781 } else {
782 panic!("Should have gotten Some(service_ref)");
783 }
784
785 let clone_opt = ctx.shared_store.get::<CloneableTestService>();
786 assert!(clone_opt.is_some(), "Should get cloned service");
787 if let Some(service_clone) = clone_opt {
788 assert_eq!(service_clone.name, "app_context_test_cloneable");
789 assert_eq!(service_clone.value, 42);
790 } else {
791 panic!("Should have gotten Some(service_clone)");
792 }
793
794 assert!(ctx.shared_store.contains::<CloneableTestService>());
795 assert!(!ctx.shared_store.contains::<String>());
796
797 let removed_cloneable_opt = ctx.shared_store.remove::<CloneableTestService>();
798 assert!(removed_cloneable_opt.is_some());
799 if let Some(removed) = removed_cloneable_opt {
800 assert_eq!(removed.name, "app_context_test_cloneable");
801 assert_eq!(removed.value, 42);
802 } else {
803 panic!("Removed cloneable option should be Some");
804 }
805 assert!(!ctx.shared_store.contains::<CloneableTestService>());
806
807 let service_non_cloneable = TestService {
808 name: "app_context_test_non_cloneable".to_string(),
809 value: 99,
810 };
811 ctx.shared_store.insert(service_non_cloneable);
812
813 let non_clone_ref_opt = ctx.shared_store.get_ref::<TestService>();
814 assert!(
815 non_clone_ref_opt.is_some(),
816 "Non-cloneable service ref should exist"
817 );
818 if let Some(service_ref) = non_clone_ref_opt {
819 assert_eq!(service_ref.name, "app_context_test_non_cloneable");
820 assert_eq!(service_ref.value, 99);
821 } else {
822 panic!("Should have gotten Some(service_ref)");
823 }
824
825 assert!(ctx.shared_store.contains::<TestService>());
826
827 let removed_non_cloneable_opt = ctx.shared_store.remove::<TestService>();
828 assert!(removed_non_cloneable_opt.is_some());
829 if let Some(removed) = removed_non_cloneable_opt {
830 assert_eq!(removed.name, "app_context_test_non_cloneable");
831 assert_eq!(removed.value, 99);
832 } else {
833 panic!("Removed non-cloneable option should be Some");
834 }
835 assert!(!ctx.shared_store.contains::<TestService>());
836 }
837}