This repository has been archived on 2026-05-05. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
el-ui-retired/vessels/el-aop/src/registry.rs
T
Will Anderson f4abfe6fdc feat: rename crates/ → vessels/ + add El ports per sub-vessel
Belated rename commit for foundation/el-ui — was missed in the
workspace-wide crates→vessels pass earlier today. Same structural
intent as the rename in the other repos: 'crates' is the Rust word,
'vessel' is El's, and the directory rename is the marker that this
slot holds an El buildable unit even if its current contents are
still Rust pending port.

Plus the El ports themselves — manifest.el + src/main.el per sub-
vessel (el-aop, el-auth, el-config, el-i18n, el-identity, el-layout,
el-platform, el-publish, el-secrets, el-services, el-style, el-ui-
compiler). The ui-compiler is a stub: elc only emits C right now;
generating browser-target JS/Wasm is the biggest open language gap
and gets its own project. Until then, el-ui-compiler emits a JS
module that throws elc.backend_missing so callers fail loudly.
Cross-repo path dependencies in Cargo.toml updated to vessels/.
2026-04-30 18:18:39 -05:00

285 lines
9.4 KiB
Rust

//! 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<dyn Fn(&HashMap<String, String>) -> Arc<dyn Aspect> + Send + Sync>;
/// Registry of aspect factories, indexed by decorator name.
pub struct AspectRegistry {
factories: RwLock<HashMap<String, AspectFactory>>,
/// When `true`, the registry is configured to prepend AuthenticateAspect
/// to every non-`@public` chain via `AspectChain::with_default_auth()`.
default_auth_enabled: RwLock<bool>,
/// Names that are bypass markers (not real aspects). Currently just "public".
bypass_markers: RwLock<std::collections::HashSet<String>>,
}
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<String, String>)]) -> 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<String, String>) -> Arc<dyn Aspect> + 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<String, String>,
) -> Option<Arc<dyn Aspect>> {
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<String> {
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");
}
}