//! Aspect registry — register built-in and custom aspects by name. //! //! The compiler's AOP codegen uses the registry to look up aspect implementations //! by their decorator name (e.g., `"authenticate"` → `AuthenticateAspect`). //! //! ## Security-by-default //! //! `"public"` is a special bypass marker — NOT an aspect. When the compiler sees //! `@public` it calls `registry.is_public_bypass(name)` and skips default auth. //! //! `set_default_auth_guard()` installs the global default `AuthenticateAspect` //! that is prepended to every non-`@public` chain. use crate::Aspect; use std::collections::HashMap; use std::sync::{Arc, RwLock}; type AspectFactory = Box) -> Arc + Send + Sync>; /// Registry of aspect factories, indexed by decorator name. pub struct AspectRegistry { factories: RwLock>, /// When `true`, the registry is configured to prepend AuthenticateAspect /// to every non-`@public` chain via `AspectChain::with_default_auth()`. default_auth_enabled: RwLock, /// Names that are bypass markers (not real aspects). Currently just "public". bypass_markers: RwLock>, } impl AspectRegistry { pub fn new() -> Self { let mut markers = std::collections::HashSet::new(); markers.insert("public".to_string()); Self { factories: RwLock::new(HashMap::new()), default_auth_enabled: RwLock::new(false), bypass_markers: RwLock::new(markers), } } /// Create a registry with all built-in aspects registered. pub fn with_builtins() -> Self { let registry = Self::new(); registry.register_builtins(); registry } /// Enable security-by-default: every non-`@public` chain will have /// `AuthenticateAspect` prepended automatically. /// /// Call at application startup. After this, use `AspectChain::with_default_auth()` /// when building chains for protected functions. pub fn set_default_auth_enabled(&self, enabled: bool) { *self.default_auth_enabled.write().expect("registry lock poisoned") = enabled; } /// Returns `true` if security-by-default auth is active. pub fn is_default_auth_enabled(&self) -> bool { *self.default_auth_enabled.read().expect("registry lock poisoned") } /// Returns `true` if `name` is a public bypass marker (e.g., `"public"`). /// /// Bypass markers are NOT aspects — they signal that default auth should /// be skipped for the decorated function. pub fn is_public_bypass(&self, name: &str) -> bool { self.bypass_markers .read() .expect("registry lock poisoned") .contains(name) } /// Register a custom bypass marker name. /// /// By default only `"public"` is registered. Use this to add custom /// bypass annotations (e.g., `"internal_only"` that uses a different guard). pub fn register_bypass_marker(&self, name: &str) { self.bypass_markers .write() .expect("registry lock poisoned") .insert(name.to_string()); } /// Build an `AspectChain` for a function with the given decorators. /// /// This is the primary chain-building entry point used by the AOP codegen. /// /// - If any decorator is a bypass marker (`@public`), returns a plain chain /// with no default auth. /// - Otherwise, if `default_auth_enabled`, prepends `AuthenticateAspect`. /// - Unknown decorator names are silently skipped (forward-compatible). pub fn build_chain(&self, decorator_names: &[(&str, HashMap)]) -> crate::AspectChain { let is_public = decorator_names.iter().any(|(name, _)| self.is_public_bypass(name)); let mut chain = crate::AspectChain::new(); for (name, params) in decorator_names { if self.is_public_bypass(name) { continue; // bypass markers are not aspects } if let Some(aspect) = self.create(name, params) { chain = chain.add(aspect); } } if !is_public && self.is_default_auth_enabled() { chain = chain.with_default_auth(); } chain } /// Register all built-in aspects. pub fn register_builtins(&self) { use crate::aspects::*; self.register("authenticate", |_params| { Arc::new(AuthenticateAspect) }); self.register("authorize", |params| { let role = params .get("role") .cloned() .unwrap_or_else(|| "user".to_string()); Arc::new(AuthorizeAspect::new(role)) }); self.register("cache", |params| { let ttl: u64 = params .get("ttl") .and_then(|s| s.parse().ok()) .unwrap_or(300); Arc::new(CacheAspect::new(ttl)) }); self.register("rate_limit", |params| { let requests: u32 = params .get("requests") .and_then(|s| s.parse().ok()) .unwrap_or(100); let per: u64 = params .get("per") .and_then(|s| s.parse().ok()) .unwrap_or(60); Arc::new(RateLimitAspect::new(requests, per)) }); self.register("log", |params| { let level = params .get("level") .cloned() .unwrap_or_else(|| "info".to_string()); Arc::new(LogAspect::new(level)) }); self.register("validate", |_params| Arc::new(ValidateAspect::new())); self.register("retry", |params| { let attempts: u32 = params .get("attempts") .and_then(|s| s.parse().ok()) .unwrap_or(3); let backoff = params .get("backoff") .map(|s| s.as_str()) .unwrap_or("none"); let aspect = RetryAspect::new(attempts); let aspect = match backoff { "exponential" => aspect.with_exponential_backoff(100), "fixed" => aspect.with_fixed_backoff(500), _ => aspect, }; Arc::new(aspect) }); self.register("trace", |params| { let service = params .get("service") .cloned() .unwrap_or_else(|| "el-ui".to_string()); Arc::new(TraceAspect::new(service)) }); } /// Register a custom aspect factory. pub fn register( &self, name: &str, factory: impl Fn(&HashMap) -> Arc + Send + Sync + 'static, ) { self.factories .write() .expect("registry lock poisoned") .insert(name.to_string(), Box::new(factory)); } /// Instantiate an aspect by decorator name with the given params. pub fn create( &self, name: &str, params: &HashMap, ) -> Option> { let factories = self.factories.read().expect("registry lock poisoned"); factories.get(name).map(|f| f(params)) } /// List all registered aspect names. pub fn aspect_names(&self) -> Vec { self.factories .read() .expect("registry lock poisoned") .keys() .cloned() .collect() } /// Check if an aspect name is registered. pub fn contains(&self, name: &str) -> bool { self.factories .read() .expect("registry lock poisoned") .contains_key(name) } } impl Default for AspectRegistry { fn default() -> Self { Self::new() } } #[cfg(test)] mod registry_default_auth_tests { use super::*; #[test] fn test_default_auth_disabled_by_default() { let reg = AspectRegistry::new(); assert!(!reg.is_default_auth_enabled()); } #[test] fn test_set_default_auth_enabled() { let reg = AspectRegistry::new(); reg.set_default_auth_enabled(true); assert!(reg.is_default_auth_enabled()); } #[test] fn test_public_is_bypass_marker() { let reg = AspectRegistry::new(); assert!(reg.is_public_bypass("public")); assert!(!reg.is_public_bypass("authenticate")); } #[test] fn test_build_chain_public_skips_default_auth() { let reg = AspectRegistry::with_builtins(); reg.set_default_auth_enabled(true); let decorators = vec![("public", HashMap::new())]; let chain = reg.build_chain(&decorators); assert!(!chain.has_auth(), "public chain should not have default auth"); } #[test] fn test_build_chain_non_public_gets_default_auth() { let reg = AspectRegistry::with_builtins(); reg.set_default_auth_enabled(true); let decorators = vec![("log", HashMap::new())]; let chain = reg.build_chain(&decorators); assert!(chain.has_auth(), "non-public chain should have default auth prepended"); } #[test] fn test_build_chain_auth_is_first_aspect() { let reg = AspectRegistry::with_builtins(); reg.set_default_auth_enabled(true); let decorators = vec![("log", HashMap::new())]; let chain = reg.build_chain(&decorators); let names = chain.aspect_names(); assert_eq!(names[0], "authenticate", "authenticate must be first in the chain"); } }