// el-aop — Aspect-Oriented Programming for el-ui. // // Each aspect has three advice points: before, after, around. The aspect // chain is composed at compile time by the el-ui-compiler from decorators. // // The El runtime model uses tagged JSON for InvocationContext so an aspect // chain composes purely by passing the context map through each advice fn. // // Built-in aspects: // @authenticate — defaults on every component (security-by-default) // @public — opt-out marker // @authorize — role/permission gate // @cache — TTL-keyed memoization // @rate_limit — per-principal token bucket // @retry — retry on error with backoff // @log — structured log around invocation // @trace — emit OpenTelemetry-shaped spans // @validate — JSON schema check on args // ── Errors ─────────────────────────────────────────────────────────────────── let AOP_ERR_UNAUTHENTICATED: String = "aop.unauthenticated" let AOP_ERR_FORBIDDEN: String = "aop.forbidden" let AOP_ERR_RATE_LIMITED: String = "aop.rate_limited" let AOP_ERR_VALIDATION: String = "aop.validation_failed" let AOP_ERR_RETRIES_EXHAUSTED: String = "aop.retries_exhausted" // ── Invocation context ─────────────────────────────────────────────────────── // // Stored as JSON: { target, method, args:{}, metadata:{} } // All advice mutates by returning a new ctx (functional style). fn ctx_new(target: String, method: String) -> String { "{\"target\":\"" + target + "\",\"method\":\"" + method + "\",\"args\":{},\"metadata\":{}}" } fn ctx_with_arg(ctx: String, key: String, value: String) -> String { json_set_path(ctx, "args." + key, "\"" + value + "\"") } fn ctx_with_meta(ctx: String, key: String, value: String) -> String { json_set_path(ctx, "metadata." + key, "\"" + value + "\"") } fn ctx_get_meta(ctx: String, key: String) -> String { json_get_path(ctx, "metadata." + key) } // ── Aspect dispatch table ──────────────────────────────────────────────────── // // Each aspect is a triple of fn names: (before, around, after). The registry // is a map from aspect name -> { before, around, after } JSON. fn registry_new() -> String { "{}" } fn registry_register(reg: String, name: String, before_fn: String, around_fn: String, after_fn: String) -> String { let entry: String = "{\"before\":\"" + before_fn + "\",\"around\":\"" + around_fn + "\",\"after\":\"" + after_fn + "\"}" json_set(reg, name, entry) } // ── @authenticate — applied by default ─────────────────────────────────────── fn aspect_authenticate_before(ctx: String) -> String { let token: String = ctx_get_meta(ctx, "authorization") if str_eq(token, "") { return ctx_with_meta(ctx, "error", AOP_ERR_UNAUTHENTICATED) } let secret: String = env("JWT_SECRET") let auth_ctx: String = auth_middleware(token, secret) let user_id: String = json_get(auth_ctx, "user_id") if str_eq(user_id, "") { return ctx_with_meta(ctx, "error", AOP_ERR_UNAUTHENTICATED) } ctx_with_meta(ctx, "user_id", user_id) } // ── @public — explicit opt-out marker ──────────────────────────────────────── fn aspect_public_before(ctx: String) -> String { ctx_with_meta(ctx, "public", "true") } // ── @authorize(role) ───────────────────────────────────────────────────────── fn aspect_authorize_before(ctx: String, required_role: String) -> String { let user_id: String = ctx_get_meta(ctx, "user_id") if str_eq(user_id, "") { return ctx_with_meta(ctx, "error", AOP_ERR_UNAUTHENTICATED) } let roles_json: String = engram_edge_traverse(user_id, "has_role") if !str_contains(roles_json, "\"" + required_role + "\"") { return ctx_with_meta(ctx, "error", AOP_ERR_FORBIDDEN) } ctx } // ── @cache(ttl_seconds) ────────────────────────────────────────────────────── fn aspect_cache_around(ctx: String, ttl: Int, proceed: String) -> String { let key: String = ctx_get_meta(ctx, "cache_key") if str_eq(key, "") { let target: String = json_get(ctx, "target") let method: String = json_get(ctx, "method") let args: String = json_get(ctx, "args") let key = sha256_hex(target + ":" + method + ":" + args) } let cached: String = cache_get(key) if !str_eq(cached, "") { return ctx_with_meta(ctx, "result", cached) } // Caller invokes proceed(ctx) externally; we record the key for `after` to use. ctx_with_meta(ctx, "cache_key", key) } fn aspect_cache_after(ctx: String, result: String, ttl: Int) -> String { let key: String = ctx_get_meta(ctx, "cache_key") if !str_eq(key, "") { cache_put(key, result, ttl) } result } // ── @rate_limit(requests, per_seconds) ─────────────────────────────────────── fn aspect_rate_limit_before(ctx: String, requests: Int, per_seconds: Int) -> String { let principal: String = ctx_get_meta(ctx, "user_id") if str_eq(principal, "") { let principal = ctx_get_meta(ctx, "ip") } let bucket_key: String = "rl:" + json_get(ctx, "target") + ":" + principal let allowed: Bool = rate_bucket_take(bucket_key, requests, per_seconds) if !allowed { return ctx_with_meta(ctx, "error", AOP_ERR_RATE_LIMITED) } ctx } // ── @retry(attempts, backoff_ms) ──────────────────────────────────────────── // // retry is necessarily an `around` aspect — it must own the loop. fn aspect_retry_around(ctx: String, attempts: Int, backoff_ms: Int, proceed_fn_name: String) -> String { let i: Int = 0 let result: String = "" while i < attempts { let result = call_dynamic(proceed_fn_name, ctx) let err: String = json_get(result, "error") if str_eq(err, "") { return result } sleep_ms(backoff_ms * (i + 1)) let i = i + 1 } ctx_with_meta(ctx, "error", AOP_ERR_RETRIES_EXHAUSTED) } // ── @log / @trace ──────────────────────────────────────────────────────────── fn aspect_log_before(ctx: String) -> String { println("[aop] -> " + json_get(ctx, "target") + "." + json_get(ctx, "method")) ctx } fn aspect_log_after(ctx: String, result: String) -> String { println("[aop] <- " + json_get(ctx, "target") + "." + json_get(ctx, "method")) result } fn aspect_trace_before(ctx: String) -> String { let span_id: String = uuid_v4() ctx_with_meta(ctx, "span_id", span_id) } // ── @validate(schema) ──────────────────────────────────────────────────────── fn aspect_validate_before(ctx: String, schema_json: String) -> String { let args: String = json_get(ctx, "args") let valid: Bool = json_schema_check(args, schema_json) if !valid { return ctx_with_meta(ctx, "error", AOP_ERR_VALIDATION) } ctx } // ── Aspect chain composition ───────────────────────────────────────────────── // // The compiler emits a call sequence like: // ctx = ctx_new(...) // ctx = aspect_authenticate_before(ctx) // ctx = aspect_log_before(ctx) // result = proceed(ctx) // result = aspect_log_after(ctx, result) // At runtime an explicit `chain_run` exists for dynamic composition. fn chain_run(ctx: String, before_fns: String, around_fn: String, after_fns: String, proceed_fn: String) -> String { let cur_ctx: String = ctx // before chain let i: Int = 0 let befs: String = before_fns while !str_eq(befs, "") { let comma: Int = str_index_of(befs, ",") let fn_name: String = befs if comma > 0 { let fn_name = str_slice(befs, 0, comma) } let cur_ctx = call_dynamic(fn_name, cur_ctx) let err: String = ctx_get_meta(cur_ctx, "error") if !str_eq(err, "") { return cur_ctx } if comma > 0 { let befs = str_slice(befs, comma + 1, str_len(befs)) } if comma < 0 { let befs = "" } } // around / proceed let result: String = call_dynamic(proceed_fn, cur_ctx) // after chain (right-to-left composition; simplified left-to-right here) let afts: String = after_fns while !str_eq(afts, "") { let comma: Int = str_index_of(afts, ",") let fn_name: String = afts if comma > 0 { let fn_name = str_slice(afts, 0, comma) } let result = call_dynamic2(fn_name, cur_ctx, result) if comma > 0 { let afts = str_slice(afts, comma + 1, str_len(afts)) } if comma < 0 { let afts = "" } } result } // ── Entry — smoke test ─────────────────────────────────────────────────────── let ctx: String = ctx_new("ProfilePage", "render") let ctx = ctx_with_arg(ctx, "user_id", "u-001") println("[el-aop] ctx = " + ctx)