dwara_core/cel/
everywhere.rs

1//! CEL everywhere (DW-059).
2//!
3//! One CEL surface across four use-sites, following the APISIX
4//! `expr`/Kong expressions-router precedent of a single expression
5//! language rather than a bespoke DSL per feature.
6//!
7//! ## Use-sites
8//!
9//! 1. **Expression matchers in routes** -- a CEL expression that
10//!    evaluates to a bool; if true, the route matches.
11//! 2. **Header/transform logic** -- a CEL expression that evaluates
12//!    to a string; the result is used as the header value.
13//! 3. **Rate-limit key derivation** -- a CEL expression that
14//!    evaluates to a string; the result is used as the rate-limit
15//!    key (e.g. `request.headers["x-api-key"]`).
16//! 4. **Policy conditions** -- a CEL expression that evaluates to a
17//!    bool; if true, the policy applies.
18//!
19//! ## Request context
20//!
21//! All four use-sites share the same request context: a `request`
22//! variable with `path`, `method`, `headers`, `query`, `host` fields.
23//! This is the standard CEL request variable that the gateway
24//! populates per-request.
25//!
26//! ## Feature gate
27//!
28//! The `cel` cargo feature must be enabled (this module builds on the
29//! DW-058 CEL engine).
30
31use std::collections::HashMap;
32
33use super::{value_to_bool, value_to_string, CelContext, CelProgram, Value};
34
35// ---------------------------------------------------------------------------
36// Request context builder
37// ---------------------------------------------------------------------------
38
39/// The standard request context for CEL evaluation.
40///
41/// All four use-sites share this context. The gateway populates it
42/// per-request with the request's path, method, headers, query, and
43/// host.
44#[derive(Debug, Clone)]
45pub struct RequestContext {
46    pub path: String,
47    pub method: String,
48    pub headers: HashMap<String, String>,
49    pub query: HashMap<String, String>,
50    pub host: String,
51}
52
53impl RequestContext {
54    /// Create a new request context.
55    pub fn new(path: &str, method: &str, host: &str) -> Self {
56        Self {
57            path: path.to_string(),
58            method: method.to_string(),
59            headers: HashMap::new(),
60            query: HashMap::new(),
61            host: host.to_string(),
62        }
63    }
64
65    /// Add a header.
66    pub fn with_header(mut self, name: &str, value: &str) -> Self {
67        self.headers.insert(name.to_string(), value.to_string());
68        self
69    }
70
71    /// Add a query parameter.
72    pub fn with_query(mut self, name: &str, value: &str) -> Self {
73        self.query.insert(name.to_string(), value.to_string());
74        self
75    }
76
77    /// Build a CelContext from this request context.
78    pub fn to_cel_context(&self) -> Result<CelContext, String> {
79        let mut ctx = CelContext::new();
80        // Build the request as a serde_json::Value so nested maps
81        // serialize correctly for the cel-interpreter.
82        let request = serde_json::json!({
83            "path": self.path,
84            "method": self.method,
85            "host": self.host,
86            "headers": self.headers,
87            "query": self.query,
88        });
89        ctx.add_var("request", &request)?;
90        Ok(ctx)
91    }
92}
93
94// ---------------------------------------------------------------------------
95// Use-site 1: Expression matchers in routes
96// ---------------------------------------------------------------------------
97
98/// A CEL-based route match condition.
99///
100/// The expression must evaluate to a bool. If true, the route
101/// matches. Compiled once at config publish time; evaluated per-request.
102#[derive(Debug)]
103pub struct RouteCondition {
104    program: CelProgram,
105}
106
107impl RouteCondition {
108    /// Compile a route condition expression.
109    pub fn compile(expr: &str) -> Result<Self, String> {
110        let program =
111            CelProgram::compile(expr).map_err(|e| format!("route condition compile: {e}"))?;
112        Ok(Self { program })
113    }
114
115    /// Evaluate the condition against a request context.
116    pub fn matches(&self, ctx: &RequestContext) -> Result<bool, String> {
117        let cel_ctx = ctx.to_cel_context()?;
118        let result = self
119            .program
120            .evaluate(&cel_ctx)
121            .map_err(|e| format!("route condition evaluate: {e}"))?;
122        value_to_bool(&result).ok_or_else(|| "route condition must evaluate to bool".to_string())
123    }
124
125    /// The original source expression.
126    pub fn source(&self) -> &str {
127        self.program.source()
128    }
129}
130
131// ---------------------------------------------------------------------------
132// Use-site 2: Header/transform logic
133// ---------------------------------------------------------------------------
134
135/// A CEL-based header transform expression.
136///
137/// The expression must evaluate to a string. The result is used as
138/// the header value. Compiled once at config publish time; evaluated
139/// per-request.
140#[derive(Debug)]
141pub struct HeaderTransform {
142    program: CelProgram,
143}
144
145impl HeaderTransform {
146    /// Compile a header transform expression.
147    pub fn compile(expr: &str) -> Result<Self, String> {
148        let program =
149            CelProgram::compile(expr).map_err(|e| format!("header transform compile: {e}"))?;
150        Ok(Self { program })
151    }
152
153    /// Evaluate the transform against a request context.
154    pub fn evaluate(&self, ctx: &RequestContext) -> Result<String, String> {
155        let cel_ctx = ctx.to_cel_context()?;
156        let result = self
157            .program
158            .evaluate(&cel_ctx)
159            .map_err(|e| format!("header transform evaluate: {e}"))?;
160        value_to_string(&result)
161            .ok_or_else(|| "header transform must evaluate to string".to_string())
162    }
163
164    /// The original source expression.
165    pub fn source(&self) -> &str {
166        self.program.source()
167    }
168}
169
170// ---------------------------------------------------------------------------
171// Use-site 3: Rate-limit key derivation
172// ---------------------------------------------------------------------------
173
174/// A CEL-based rate-limit key derivation expression.
175///
176/// The expression must evaluate to a string. The result is used as
177/// the rate-limit key. Compiled once at config publish time; evaluated
178/// per-request.
179#[derive(Debug)]
180pub struct RateLimitKey {
181    program: CelProgram,
182}
183
184impl RateLimitKey {
185    /// Compile a rate-limit key expression.
186    pub fn compile(expr: &str) -> Result<Self, String> {
187        let program =
188            CelProgram::compile(expr).map_err(|e| format!("rate-limit key compile: {e}"))?;
189        Ok(Self { program })
190    }
191
192    /// Evaluate the key derivation against a request context.
193    pub fn derive(&self, ctx: &RequestContext) -> Result<String, String> {
194        let cel_ctx = ctx.to_cel_context()?;
195        let result = self
196            .program
197            .evaluate(&cel_ctx)
198            .map_err(|e| format!("rate-limit key evaluate: {e}"))?;
199        value_to_string(&result).ok_or_else(|| "rate-limit key must evaluate to string".to_string())
200    }
201
202    /// The original source expression.
203    pub fn source(&self) -> &str {
204        self.program.source()
205    }
206}
207
208// ---------------------------------------------------------------------------
209// Use-site 4: Policy conditions
210// ---------------------------------------------------------------------------
211
212/// A CEL-based policy condition.
213///
214/// The expression must evaluate to a bool. If true, the policy
215/// applies. Compiled once at config publish time; evaluated per-request.
216#[derive(Debug)]
217pub struct PolicyCondition {
218    program: CelProgram,
219}
220
221impl PolicyCondition {
222    /// Compile a policy condition expression.
223    pub fn compile(expr: &str) -> Result<Self, String> {
224        let program =
225            CelProgram::compile(expr).map_err(|e| format!("policy condition compile: {e}"))?;
226        Ok(Self { program })
227    }
228
229    /// Evaluate the condition against a request context.
230    pub fn applies(&self, ctx: &RequestContext) -> Result<bool, String> {
231        let cel_ctx = ctx.to_cel_context()?;
232        let result = self
233            .program
234            .evaluate(&cel_ctx)
235            .map_err(|e| format!("policy condition evaluate: {e}"))?;
236        value_to_bool(&result).ok_or_else(|| "policy condition must evaluate to bool".to_string())
237    }
238
239    /// The original source expression.
240    pub fn source(&self) -> &str {
241        self.program.source()
242    }
243}
244
245// ---------------------------------------------------------------------------
246// Unified API: compile any use-site
247// ---------------------------------------------------------------------------
248
249/// The kind of CEL use-site.
250#[derive(Debug, Clone, Copy, PartialEq, Eq)]
251pub enum CelUseSite {
252    RouteCondition,
253    HeaderTransform,
254    RateLimitKey,
255    PolicyCondition,
256}
257
258impl CelUseSite {
259    /// The human-readable name.
260    pub fn name(&self) -> &'static str {
261        match self {
262            CelUseSite::RouteCondition => "route condition",
263            CelUseSite::HeaderTransform => "header transform",
264            CelUseSite::RateLimitKey => "rate-limit key",
265            CelUseSite::PolicyCondition => "policy condition",
266        }
267    }
268
269    /// The expected result type.
270    pub fn expected_type(&self) -> &'static str {
271        match self {
272            CelUseSite::RouteCondition | CelUseSite::PolicyCondition => "bool",
273            CelUseSite::HeaderTransform | CelUseSite::RateLimitKey => "string",
274        }
275    }
276}
277
278/// Compile a CEL expression for a specific use-site.
279pub fn compile_for(use_site: CelUseSite, expr: &str) -> Result<CelProgram, String> {
280    CelProgram::compile(expr).map_err(|e| format!("{} compile: {e}", use_site.name()))
281}
282
283/// Evaluate a CEL program for a specific use-site, checking the result type.
284pub fn evaluate_for(
285    use_site: CelUseSite,
286    program: &CelProgram,
287    ctx: &RequestContext,
288) -> Result<Value, String> {
289    let cel_ctx = ctx.to_cel_context()?;
290    let result = program
291        .evaluate(&cel_ctx)
292        .map_err(|e| format!("{} evaluate: {e}", use_site.name()))?;
293
294    // Type-check the result.
295    let type_ok = match use_site {
296        CelUseSite::RouteCondition | CelUseSite::PolicyCondition => {
297            matches!(result, Value::Bool(_))
298        }
299        CelUseSite::HeaderTransform | CelUseSite::RateLimitKey => {
300            matches!(result, Value::String(_))
301        }
302    };
303
304    if !type_ok {
305        return Err(format!(
306            "{} must evaluate to {}, got {:?}",
307            use_site.name(),
308            use_site.expected_type(),
309            result
310        ));
311    }
312
313    Ok(result)
314}