//! Built-in aspects for el-ui. use std::{ collections::HashMap, sync::{ atomic::{AtomicU64, Ordering}, Mutex, }, time::{Duration, Instant}, }; use crate::{AopError, AopResult, Aspect, InvocationContext, InvocationResult, ProceedFn}; // ── @authenticate ───────────────────────────────────────────────────────────── /// `@authenticate` — Requires a valid session before the method executes. /// /// Checks `ctx.metadata["session_token"]` or `ctx.metadata["user_id"]`. /// If absent, rejects with `AopError::Unauthenticated`. pub struct AuthenticateAspect; impl Aspect for AuthenticateAspect { fn name(&self) -> &'static str { "authenticate" } fn before(&self, ctx: &mut InvocationContext) -> AopResult<()> { // Look for a session token or user ID in metadata. // In production, the auth middleware populates these from the JWT/session. let has_user = ctx.metadata.contains_key("user_id") || ctx.metadata.contains_key("session_token"); if !has_user { return Err(AopError::Unauthenticated); } Ok(()) } } // ── @authorize ──────────────────────────────────────────────────────────────── /// `@authorize(role: "admin")` — Requires the caller to have a specific role. pub struct AuthorizeAspect { pub required_role: String, } impl AuthorizeAspect { pub fn new(role: impl Into) -> Self { Self { required_role: role.into() } } } impl Aspect for AuthorizeAspect { fn name(&self) -> &'static str { "authorize" } fn before(&self, ctx: &mut InvocationContext) -> AopResult<()> { let user_roles = ctx .metadata .get("roles") .map(|s| s.as_str()) .unwrap_or(""); // Roles are stored as comma-separated string: "admin,user" let has_role = user_roles .split(',') .any(|r| r.trim() == self.required_role.as_str()); if !has_role { return Err(AopError::Forbidden { role: self.required_role.clone(), }); } Ok(()) } } // ── @cache ──────────────────────────────────────────────────────────────────── struct CacheEntry { value: InvocationResult, inserted_at: Instant, ttl: Duration, } impl CacheEntry { fn is_expired(&self) -> bool { self.inserted_at.elapsed() > self.ttl } } /// `@cache(ttl: 300)` — Cache method responses for `ttl` seconds. /// /// Cache key is `"target::method::{args_sorted_json}"`. pub struct CacheAspect { pub ttl: Duration, cache: Mutex>, } impl CacheAspect { pub fn new(ttl_seconds: u64) -> Self { Self { ttl: Duration::from_secs(ttl_seconds), cache: Mutex::new(HashMap::new()), } } fn cache_key(ctx: &InvocationContext) -> String { let mut pairs: Vec<(&String, &String)> = ctx.args.iter().collect(); pairs.sort_by_key(|(k, _)| k.as_str()); let args = pairs .iter() .map(|(k, v)| format!("{}={}", k, v)) .collect::>() .join(","); format!("{}::{}::{}", ctx.target, ctx.method, args) } } impl Aspect for CacheAspect { fn name(&self) -> &'static str { "cache" } fn around( &self, ctx: InvocationContext, proceed: &ProceedFn, ) -> AopResult { let key = Self::cache_key(&ctx); // Check cache { let cache = self.cache.lock().expect("cache lock poisoned"); if let Some(entry) = cache.get(&key) { if !entry.is_expired() { return Ok(entry.value.clone()); } } } // Cache miss — proceed and store result let result = proceed(ctx)?; { let mut cache = self.cache.lock().expect("cache lock poisoned"); // Evict expired entries while we're here cache.retain(|_, v| !v.is_expired()); cache.insert( key, CacheEntry { value: result.clone(), inserted_at: Instant::now(), ttl: self.ttl, }, ); } Ok(result) } } // ── @rate_limit ─────────────────────────────────────────────────────────────── struct RateWindow { count: u32, window_start: Instant, window_duration: Duration, } /// `@rate_limit(requests: 100, per: 60)` — Allow at most `requests` calls per `per` seconds. pub struct RateLimitAspect { pub max_requests: u32, pub window: Duration, state: Mutex>, } impl RateLimitAspect { pub fn new(max_requests: u32, per_seconds: u64) -> Self { Self { max_requests, window: Duration::from_secs(per_seconds), state: Mutex::new(HashMap::new()), } } /// The rate-limit key for a caller. Uses `user_id` or "anonymous". fn caller_key(ctx: &InvocationContext) -> String { ctx.metadata .get("user_id") .cloned() .unwrap_or_else(|| "anonymous".to_string()) } } impl Aspect for RateLimitAspect { fn name(&self) -> &'static str { "rate_limit" } fn before(&self, ctx: &mut InvocationContext) -> AopResult<()> { let key = Self::caller_key(ctx); let mut state = self.state.lock().expect("rate limit lock poisoned"); let now = Instant::now(); let window = state.entry(key).or_insert(RateWindow { count: 0, window_start: now, window_duration: self.window, }); // Reset window if expired if now.duration_since(window.window_start) >= window.window_duration { window.count = 0; window.window_start = now; } if window.count >= self.max_requests { return Err(AopError::RateLimited { requests: self.max_requests, per: self.window.as_secs(), }); } window.count += 1; Ok(()) } } // ── @log ────────────────────────────────────────────────────────────────────── /// `@log(level: "info")` — Structured logging for every method call. pub struct LogAspect { pub level: String, } impl LogAspect { pub fn new(level: impl Into) -> Self { Self { level: level.into() } } } impl Aspect for LogAspect { fn name(&self) -> &'static str { "log" } fn around( &self, ctx: InvocationContext, proceed: &ProceedFn, ) -> AopResult { // In production: use the `tracing` crate with the appropriate level macro. let _log_entry = format!( "[{}] {}.{}({:?})", self.level.to_uppercase(), ctx.target, ctx.method, ctx.args ); let result = proceed(ctx.clone()); let _log_result = match &result { Ok(r) => format!("[{}] {}.{} → ok: {}", self.level.to_uppercase(), ctx.target, ctx.method, r.value), Err(e) => format!("[ERROR] {}.{} → err: {}", ctx.target, ctx.method, e), }; result } } // ── @validate ───────────────────────────────────────────────────────────────── /// `@validate` — Run input validation before the method executes. /// /// Validation rules are registered per method. If no rules are registered, /// the aspect passes through (fail-open for ease of adoption). pub struct ValidateAspect { /// `"target::method"` → list of validation rules (field, rule_name) rules: Mutex>>, } impl ValidateAspect { pub fn new() -> Self { Self { rules: Mutex::new(HashMap::new()), } } /// Add a validation rule. `rule` is one of: "required", "email", "min:N", "max:N". pub fn add_rule( &self, target: &str, method: &str, field: impl Into, rule: impl Into, ) { let key = format!("{}::{}", target, method); self.rules .lock() .expect("validate lock poisoned") .entry(key) .or_default() .push((field.into(), rule.into())); } fn validate_field(value: &str, rule: &str) -> AopResult<()> { if rule == "required" && value.trim().is_empty() { return Err(AopError::ValidationFailed("field is required".into())); } if rule == "email" && !value.contains('@') { return Err(AopError::ValidationFailed(format!( "'{}' is not a valid email", value ))); } if let Some(min_str) = rule.strip_prefix("min:") { let min: usize = min_str.parse().unwrap_or(0); if value.len() < min { return Err(AopError::ValidationFailed(format!( "minimum length is {}", min ))); } } if let Some(max_str) = rule.strip_prefix("max:") { let max: usize = max_str.parse().unwrap_or(usize::MAX); if value.len() > max { return Err(AopError::ValidationFailed(format!( "maximum length is {}", max ))); } } Ok(()) } } impl Default for ValidateAspect { fn default() -> Self { Self::new() } } impl Aspect for ValidateAspect { fn name(&self) -> &'static str { "validate" } fn before(&self, ctx: &mut InvocationContext) -> AopResult<()> { let key = format!("{}::{}", ctx.target, ctx.method); let rules = self.rules.lock().expect("validate lock poisoned"); if let Some(field_rules) = rules.get(&key) { for (field, rule) in field_rules { let value = ctx.args.get(field).map(|s| s.as_str()).unwrap_or(""); Self::validate_field(value, rule)?; } } Ok(()) } } // ── @retry ──────────────────────────────────────────────────────────────────── /// `@retry(attempts: 3, backoff: "exponential")` — Retry on failure. pub struct RetryAspect { pub attempts: u32, pub backoff: BackoffStrategy, } #[derive(Debug, Clone, PartialEq)] pub enum BackoffStrategy { None, Fixed(Duration), Exponential { base: Duration }, } impl RetryAspect { pub fn new(attempts: u32) -> Self { Self { attempts, backoff: BackoffStrategy::None } } pub fn with_exponential_backoff(mut self, base_ms: u64) -> Self { self.backoff = BackoffStrategy::Exponential { base: Duration::from_millis(base_ms), }; self } pub fn with_fixed_backoff(mut self, ms: u64) -> Self { self.backoff = BackoffStrategy::Fixed(Duration::from_millis(ms)); self } fn sleep_duration(&self, attempt: u32) -> Duration { match &self.backoff { BackoffStrategy::None => Duration::ZERO, BackoffStrategy::Fixed(d) => *d, BackoffStrategy::Exponential { base } => { // base * 2^attempt, capped at 30s let factor = 1u64 << attempt.min(10); std::cmp::min(*base * factor as u32, Duration::from_secs(30)) } } } } impl Aspect for RetryAspect { fn name(&self) -> &'static str { "retry" } fn around( &self, ctx: InvocationContext, proceed: &ProceedFn, ) -> AopResult { let mut last_error = String::new(); for attempt in 0..self.attempts { match proceed(ctx.clone()) { Ok(result) => return Ok(result), Err(e) => { last_error = e.to_string(); let sleep_for = self.sleep_duration(attempt); if sleep_for > Duration::ZERO && attempt + 1 < self.attempts { std::thread::sleep(sleep_for); } } } } Err(AopError::RetriesExhausted { attempts: self.attempts, last_error, }) } } // ── @trace ──────────────────────────────────────────────────────────────────── static TRACE_COUNTER: AtomicU64 = AtomicU64::new(1); /// `@trace` — Add a distributed tracing span to every method call. /// /// Injects a `trace_id` and `span_id` into context metadata. /// In production, emit the span to an OpenTelemetry collector. pub struct TraceAspect { pub service_name: String, } impl TraceAspect { pub fn new(service_name: impl Into) -> Self { Self { service_name: service_name.into() } } fn new_span_id() -> String { let id = TRACE_COUNTER.fetch_add(1, Ordering::Relaxed); format!("span-{:016x}", id) } } impl Aspect for TraceAspect { fn name(&self) -> &'static str { "trace" } fn around( &self, mut ctx: InvocationContext, proceed: &ProceedFn, ) -> AopResult { // Create or inherit trace ID let trace_id = ctx .metadata .get("trace_id") .cloned() .unwrap_or_else(|| format!("trace-{:016x}", TRACE_COUNTER.load(Ordering::Relaxed))); let span_id = Self::new_span_id(); ctx.metadata.insert("trace_id".into(), trace_id.clone()); ctx.metadata.insert("span_id".into(), span_id.clone()); let start = Instant::now(); let result = proceed(ctx.clone()); let duration_us = start.elapsed().as_micros(); // In production: emit span to OpenTelemetry: // tracer.start_with_context("method_call", parent_cx) // .set_attribute(KeyValue::new("service", self.service_name.clone())) // .set_attribute(KeyValue::new("method", ctx.method.clone())) // .set_attribute(KeyValue::new("duration_us", duration_us as i64)) // .end(); let _ = duration_us; result.map(|mut r| { r.metadata.insert("trace_id".into(), trace_id); r.metadata.insert("span_id".into(), span_id); r }) } }