Archived
158 lines
4.6 KiB
Rust
158 lines
4.6 KiB
Rust
//! 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<T> = Result<T, AopError>;
|
|
|
|
/// 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<String, String>,
|
|
/// Metadata added by aspects (e.g., the authenticated user, trace ID).
|
|
pub metadata: HashMap<String, String>,
|
|
}
|
|
|
|
impl InvocationContext {
|
|
pub fn new(target: impl Into<String>, method: impl Into<String>) -> Self {
|
|
Self {
|
|
target: target.into(),
|
|
method: method.into(),
|
|
args: HashMap::new(),
|
|
metadata: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
pub fn with_arg(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
|
|
self.args.insert(key.into(), value.into());
|
|
self
|
|
}
|
|
|
|
pub fn with_meta(mut self, key: impl Into<String>, value: impl Into<String>) -> 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<String, String>,
|
|
}
|
|
|
|
impl InvocationResult {
|
|
pub fn new(value: impl Into<String>) -> Self {
|
|
Self {
|
|
value: value.into(),
|
|
metadata: HashMap::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A handler that performs the actual method invocation.
|
|
/// Aspects wrap around this.
|
|
pub type ProceedFn = Box<dyn Fn(InvocationContext) -> AopResult<InvocationResult> + 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<InvocationResult>,
|
|
) -> AopResult<InvocationResult> {
|
|
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<InvocationResult> {
|
|
self.before(&mut ctx)?;
|
|
let result = proceed(ctx.clone());
|
|
self.after(&ctx, result)
|
|
}
|
|
}
|