Skip to main content

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}