riglr_core/
provider.rs

1//! Chain-agnostic application context for dependency injection
2//!
3//! This module provides the `ApplicationContext` pattern for managing shared resources
4//! and dependencies across the application without circular dependencies.
5//!
6//! # Architecture
7//!
8//! The ApplicationContext enables riglr-core to remain chain-agnostic by using
9//! type erasure and dependency injection. Concrete blockchain implementations
10//! are injected at runtime by the application layer.
11//!
12//! # Dependency Flow
13//!
14//! ```text
15//! Application Layer (creates clients)
16//!         ↓
17//! ApplicationContext (stores as Arc<dyn Any>)
18//!         ↓
19//! Tools/Workers (retrieve by type)
20//! ```
21
22use dashmap::DashMap;
23use riglr_config::Config;
24use std::any::{Any, TypeId};
25use std::sync::Arc;
26use std::time::Duration;
27
28use crate::util::RateLimiter;
29
30/// Chain-agnostic context for dependency injection and resource management.
31///
32/// The ApplicationContext serves as a dependency injection container for
33/// the riglr ecosystem, enabling tools and workers to access shared resources
34/// (RPC clients, signers, database connections) without creating circular dependencies.
35///
36/// # Chain-Agnostic Design
37///
38/// The context uses type erasure (`Arc<dyn Any>`) to store blockchain clients
39/// without depending on their concrete types. This allows riglr-core to remain
40/// independent of blockchain SDKs while still providing access to them at runtime.
41///
42/// # Extension System
43///
44/// Resources are stored as "extensions" - type-erased objects that can be
45/// retrieved by their original type. This enables clean separation between
46/// riglr-core (which defines the interfaces) and chain-specific crates
47/// (which provide the implementations).
48///
49/// ## Accessing Chain-Specific Clients
50///
51/// To maintain its chain-agnostic nature, `riglr-core` does not have direct methods
52/// like `solana_client()` on the `ApplicationContext`. Instead, these ergonomic
53/// accessors are provided via extension traits in chain-specific crates.
54///
55/// For example, `riglr-solana-tools` provides the `SolanaAppContextProvider` trait,
56/// which adds the `.solana_client()` method to `ApplicationContext`.
57///
58/// See the [`riglr_core::provider_extensions`] module for more details on this pattern.
59///
60/// # Examples
61///
62/// ```rust,no_run
63/// use riglr_core::provider::ApplicationContext;
64/// use riglr_config::ConfigBuilder;
65/// use std::sync::Arc;
66///
67/// // Application layer creates context and injects dependencies
68/// let config = ConfigBuilder::default().build().unwrap();
69/// let context = ApplicationContext::from_config(&config);
70///
71/// // Inject Solana RPC client (in real code, from riglr-solana-tools)
72/// // let solana_client = Arc::new(solana_client::rpc_client::RpcClient::new(...));
73/// // context.set_extension(solana_client.clone());
74///
75/// // Inject EVM provider (in real code, from riglr-evm-tools)  
76/// // let evm_provider = Arc::new(alloy::providers::Provider::new(...));
77/// // context.set_extension(evm_provider.clone());
78///
79/// // Tools retrieve clients by type
80/// // let client: Option<Arc<RpcClient>> = context.get_extension();
81/// ```
82#[derive(Clone, Debug)]
83pub struct ApplicationContext {
84    /// Configuration for the application
85    pub config: Config,
86    /// Rate limiter for controlling request rates per client/user
87    pub rate_limiter: Arc<RateLimiter>,
88    /// Type-safe extensions for storing arbitrary shared resources
89    extensions: Arc<DashMap<TypeId, Arc<dyn Any + Send + Sync>>>,
90}
91
92impl ApplicationContext {
93    /// Create a new ApplicationContext from configuration
94    pub fn from_config(config: &Config) -> Self {
95        // Initialize rate limiter with default values
96        // Default: 100 requests per minute per client
97        let rate_limiter = Arc::new(RateLimiter::new(100, Duration::from_secs(60)));
98
99        Self {
100            config: config.clone(),
101            rate_limiter,
102            extensions: Arc::new(DashMap::new()),
103        }
104    }
105
106    /// Create a new ApplicationContext from environment variables
107    ///
108    /// **DEPRECATED**: Configuration should be loaded in the application binary using
109    /// `riglr_config::Config::from_env()` and passed to `ApplicationContext::from_config()`.
110    /// This ensures proper separation of concerns where `riglr-core` consumes configuration
111    /// but does not load it, reinforcing the unidirectional dependency flow.
112    ///
113    /// # Migration Guide
114    ///
115    /// Instead of:
116    /// ```rust,no_run
117    /// use riglr_core::provider::ApplicationContext;
118    /// let context = ApplicationContext::from_env();
119    /// ```
120    ///
121    /// Use:
122    /// ```rust,no_run
123    /// use riglr_core::provider::ApplicationContext;
124    /// use riglr_config::Config;
125    ///
126    /// let config = Config::from_env();
127    /// let context = ApplicationContext::from_config(&config);
128    /// ```
129    #[deprecated(
130        since = "0.3.0",
131        note = "Use Config::from_env() followed by ApplicationContext::from_config() instead. This ensures proper separation of concerns."
132    )]
133    pub fn from_env() -> Self {
134        let config = Config::from_env();
135        Self::from_config(&config)
136    }
137
138    /// Add an extension to the context
139    ///
140    /// Extensions are stored by their type, allowing type-safe retrieval later.
141    /// This is the recommended pattern for injecting RPC clients and other resources.
142    ///
143    /// # Examples
144    ///
145    /// ```rust,no_run
146    /// use riglr_core::provider::ApplicationContext;
147    /// use riglr_config::ConfigBuilder;
148    /// use std::sync::Arc;
149    ///
150    /// let config = ConfigBuilder::default().build().unwrap();
151    /// let context = ApplicationContext::from_config(&config);
152    ///
153    /// // Add blockchain RPC clients as extensions
154    /// // Example: Add Solana RPC client
155    /// // let solana_client = Arc::new(solana_client::rpc_client::RpcClient::new(...));
156    /// // context.set_extension(solana_client);
157    ///
158    /// // Example: Add EVM provider
159    /// // let evm_provider = Arc::new(alloy::Provider::new(...));
160    /// // context.set_extension(evm_provider);
161    /// ```
162    pub fn set_extension<T: Send + Sync + 'static>(&self, extension: Arc<T>) {
163        self.extensions.insert(TypeId::of::<T>(), extension);
164    }
165
166    /// Get an extension by type
167    ///
168    /// Returns None if no extension of the given type has been set.
169    ///
170    /// # Examples
171    ///
172    /// ```rust,no_run
173    /// use riglr_core::provider::ApplicationContext;
174    /// use riglr_config::ConfigBuilder;
175    /// use std::sync::Arc;
176    ///
177    /// let config = ConfigBuilder::default().build().unwrap();
178    /// let context = ApplicationContext::from_config(&config);
179    /// // Add a typed extension (e.g., an RPC client)
180    /// // let client = Arc::new(MyRpcClient::new(...));
181    /// // context.set_extension(client.clone());
182    ///
183    /// // Retrieve the client later by type
184    /// // let retrieved: Arc<MyRpcClient> = context.get_extension()
185    ///     // .expect("RPC client not found");
186    /// ```
187    pub fn get_extension<T: Send + Sync + 'static>(&self) -> Option<Arc<T>> {
188        self.extensions
189            .get(&TypeId::of::<T>())
190            .and_then(|ext| ext.clone().downcast::<T>().ok())
191    }
192
193    /// Check if an extension of the given type exists
194    pub fn has_extension<T: Send + Sync + 'static>(&self) -> bool {
195        self.extensions.contains_key(&TypeId::of::<T>())
196    }
197
198    /// Remove an extension by type
199    ///
200    /// Returns the removed extension if it existed.
201    pub fn remove_extension<T: Send + Sync + 'static>(&self) -> Option<Arc<T>> {
202        self.extensions
203            .remove(&TypeId::of::<T>())
204            .and_then(|(_, ext)| ext.downcast::<T>().ok())
205    }
206
207    /// Clear all extensions
208    pub fn clear_extensions(&self) {
209        self.extensions.clear();
210    }
211
212    /// Get the number of extensions
213    pub fn extension_count(&self) -> usize {
214        self.extensions.len()
215    }
216}
217
218impl Default for ApplicationContext {
219    fn default() -> Self {
220        // Create with an empty/default configuration using builder
221        // Users should use from_config() for production use
222        let config = riglr_config::ConfigBuilder::new()
223            .build()
224            .expect("Default config should be valid");
225        let rate_limiter = Arc::new(RateLimiter::new(100, Duration::from_secs(60)));
226
227        Self {
228            config,
229            rate_limiter,
230            extensions: Arc::new(DashMap::new()),
231        }
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238
239    #[derive(Debug, Clone)]
240    struct TestResource {
241        value: String,
242    }
243
244    #[test]
245    fn test_application_context_extensions() {
246        let context = ApplicationContext::default();
247
248        // Test adding and retrieving an extension
249        let resource = Arc::new(TestResource {
250            value: "test".to_string(),
251        });
252        context.set_extension(resource.clone());
253
254        let retrieved: Arc<TestResource> = context.get_extension().expect("Resource not found");
255        assert_eq!(retrieved.value, "test");
256    }
257
258    #[test]
259    fn test_application_context_multiple_extensions() {
260        let context = ApplicationContext::default();
261
262        // Add multiple different types
263        let resource1 = Arc::new(TestResource {
264            value: "test1".to_string(),
265        });
266        let resource2 = Arc::new(42u32);
267
268        context.set_extension(resource1.clone());
269        context.set_extension(resource2.clone());
270
271        // Retrieve both
272        let retrieved1: Arc<TestResource> = context.get_extension().expect("Resource not found");
273        let retrieved2: Arc<u32> = context.get_extension().expect("u32 not found");
274
275        assert_eq!(retrieved1.value, "test1");
276        assert_eq!(*retrieved2, 42);
277    }
278
279    #[test]
280    fn test_application_context_has_extension() {
281        let context = ApplicationContext::default();
282
283        assert!(!context.has_extension::<TestResource>());
284
285        let resource = Arc::new(TestResource {
286            value: "test".to_string(),
287        });
288        context.set_extension(resource);
289
290        assert!(context.has_extension::<TestResource>());
291    }
292
293    #[test]
294    fn test_application_context_remove_extension() {
295        let context = ApplicationContext::default();
296
297        let resource = Arc::new(TestResource {
298            value: "test".to_string(),
299        });
300        context.set_extension(resource);
301
302        assert!(context.has_extension::<TestResource>());
303
304        let removed: Option<Arc<TestResource>> = context.remove_extension();
305        assert!(removed.is_some());
306        assert!(!context.has_extension::<TestResource>());
307    }
308}