//! el-aop — Aspect-Oriented Programming for el-ui. //! //! Cross-cutting concerns as first-class language features. Not a library you //! import. Built into the framework. Applied as decorators: //! //! ```text //! @authenticate ← applied by DEFAULT to every function //! @authorize(role: "admin") //! @cache(ttl: 300) //! @rate_limit(requests: 100, per: 60) //! component AdminDashboard { ... } //! //! @public ← explicit opt-out of authentication //! component LandingPage { ... } //! ``` //! //! ## Security-by-default //! //! `@authenticate` is the default. Functions without `@public` are protected. //! This makes it as hard as possible to accidentally ship an unprotected endpoint. pub mod aspects; pub mod chain; pub mod registry; pub use aspects::{ AuthenticateAspect, AuthorizeAspect, CacheAspect, LogAspect, RateLimitAspect, RetryAspect, TraceAspect, ValidateAspect, }; pub use chain::AspectChain; pub use public::PublicMarker; pub use registry::AspectRegistry; pub mod public; #[cfg(test)] mod tests; use std::collections::HashMap; use thiserror::Error; #[derive(Debug, Error)] pub enum AopError { #[error("authentication required")] Unauthenticated, #[error("forbidden: requires role '{role}'")] Forbidden { role: String }, #[error("rate limit exceeded: {requests} requests per {per}s")] RateLimited { requests: u32, per: u64 }, #[error("validation failed: {0}")] ValidationFailed(String), #[error("aspect error: {0}")] Aspect(String), #[error("all {attempts} retry attempts failed: {last_error}")] RetriesExhausted { attempts: u32, last_error: String }, } pub type AopResult = Result; /// Context passed through the aspect chain. /// /// Contains the incoming arguments and metadata about the call. /// Aspects can read and mutate this context as they execute. #[derive(Debug, Clone)] pub struct InvocationContext { /// The component or service being called. pub target: String, /// The method being called. pub method: String, /// Arguments passed to the method. pub args: HashMap, /// Metadata added by aspects (e.g., the authenticated user, trace ID). pub metadata: HashMap, } impl InvocationContext { pub fn new(target: impl Into, method: impl Into) -> Self { Self { target: target.into(), method: method.into(), args: HashMap::new(), metadata: HashMap::new(), } } pub fn with_arg(mut self, key: impl Into, value: impl Into) -> Self { self.args.insert(key.into(), value.into()); self } pub fn with_meta(mut self, key: impl Into, value: impl Into) -> Self { self.metadata.insert(key.into(), value.into()); self } pub fn get_meta(&self, key: &str) -> Option<&str> { self.metadata.get(key).map(|s| s.as_str()) } } /// The result of invoking a method through an aspect chain. #[derive(Debug, Clone)] pub struct InvocationResult { pub value: String, pub metadata: HashMap, } impl InvocationResult { pub fn new(value: impl Into) -> Self { Self { value: value.into(), metadata: HashMap::new(), } } } /// A handler that performs the actual method invocation. /// Aspects wrap around this. pub type ProceedFn = Box AopResult + Send + Sync>; /// The core Aspect trait. /// /// Each aspect implements `before`, `after`, or `around` advice. /// The default implementations are no-ops — only override what you need. pub trait Aspect: Send + Sync { /// The aspect's name (used for debugging and registry lookup). fn name(&self) -> &'static str; /// Before advice — runs before the method. Can reject the call. fn before(&self, ctx: &mut InvocationContext) -> AopResult<()> { let _ = ctx; Ok(()) } /// After advice — runs after the method. Receives the result. /// Can modify the result or perform cleanup. fn after( &self, ctx: &InvocationContext, result: AopResult, ) -> AopResult { let _ = ctx; result } /// Around advice — wraps the entire invocation. /// The default implementation calls `before`, then `proceed`, then `after`. fn around( &self, mut ctx: InvocationContext, proceed: &ProceedFn, ) -> AopResult { self.before(&mut ctx)?; let result = proceed(ctx.clone()); self.after(&ctx, result) } }