Skip to main content

kube_cel/
lib.rs

1//! Kubernetes CEL extension functions for the `cel` crate.
2//!
3//! This crate provides the Kubernetes-specific CEL (Common Expression Language) functions
4//! that are available in Kubernetes CRD validation rules, built on top of the `cel` crate.
5//!
6//! # Usage
7//!
8//! ```rust
9//! use cel::Context;
10//! use kube_cel::register_all;
11//!
12//! let mut ctx = Context::default();
13//! register_all(&mut ctx);
14//! ```
15//!
16//! # CRD Validation Pipeline (feature = `validation`)
17//!
18//! Compile and evaluate `x-kubernetes-validations` CEL rules client-side,
19//! without an API server.
20//!
21//! ```toml
22//! kube-cel = { version = "0.4", features = ["validation"] }
23//! ```
24//!
25//! ```rust,ignore
26//! use kube_cel::validation::Validator;
27//! use serde_json::json;
28//!
29//! let schema = json!({
30//!     "type": "object",
31//!     "x-kubernetes-validations": [
32//!         {"rule": "self.replicas >= 0", "message": "must be non-negative"}
33//!     ],
34//!     "properties": { "replicas": {"type": "integer"} }
35//! });
36//!
37//! let object = json!({"replicas": -1});
38//! let errors = Validator::new().validate(&schema, &object, None);
39//! assert_eq!(errors.len(), 1);
40//! ```
41//!
42//! For repeated validation against the same schema, pre-compile with
43//! [`compilation::compile_schema`] and use [`validation::Validator::validate_compiled`].
44
45#[cfg(feature = "strings")]
46pub mod strings;
47
48#[cfg(feature = "lists")]
49pub mod lists;
50
51#[cfg(feature = "sets")]
52pub mod sets;
53
54#[cfg(feature = "regex_funcs")]
55pub mod regex_funcs;
56
57#[cfg(feature = "urls")]
58pub mod urls;
59
60#[cfg(feature = "ip")]
61pub mod ip;
62
63#[cfg(feature = "semver_funcs")]
64pub mod semver_funcs;
65
66#[cfg(feature = "format")]
67pub mod format;
68
69#[cfg(feature = "quantity")]
70pub mod quantity;
71
72#[cfg(feature = "jsonpatch")]
73pub mod jsonpatch;
74
75#[cfg(feature = "named_format")]
76pub mod named_format;
77
78#[cfg(feature = "math")]
79pub mod math;
80
81#[cfg(feature = "encoders")]
82pub mod encoders;
83
84#[cfg(feature = "validation")]
85pub mod escaping;
86
87#[cfg(feature = "validation")]
88pub mod values;
89
90#[cfg(feature = "validation")]
91pub mod compilation;
92
93#[cfg(feature = "validation")]
94pub mod validation;
95
96mod dispatch;
97mod value_ops;
98
99/// Register all available Kubernetes CEL extension functions into the given context.
100pub fn register_all(ctx: &mut cel::Context<'_>) {
101    #[cfg(feature = "strings")]
102    strings::register(ctx);
103
104    #[cfg(feature = "lists")]
105    lists::register(ctx);
106
107    #[cfg(feature = "sets")]
108    sets::register(ctx);
109
110    #[cfg(feature = "regex_funcs")]
111    regex_funcs::register(ctx);
112
113    #[cfg(feature = "urls")]
114    urls::register(ctx);
115
116    #[cfg(feature = "ip")]
117    ip::register(ctx);
118
119    #[cfg(feature = "semver_funcs")]
120    semver_funcs::register(ctx);
121
122    #[cfg(feature = "format")]
123    format::register(ctx);
124
125    #[cfg(feature = "quantity")]
126    quantity::register(ctx);
127
128    #[cfg(feature = "jsonpatch")]
129    jsonpatch::register(ctx);
130
131    #[cfg(feature = "named_format")]
132    named_format::register(ctx);
133
134    #[cfg(feature = "math")]
135    math::register(ctx);
136
137    #[cfg(feature = "encoders")]
138    encoders::register(ctx);
139
140    // Dispatch: registers functions with name collisions (indexOf, reverse,
141    // min/max, string, ip, isGreaterThan, etc.). Order-independent since
142    // individual modules no longer register these conflicting names.
143    dispatch::register(ctx);
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[allow(unused_imports)]
151    use std::sync::Arc;
152
153    use cel::{Context, Program, Value};
154
155    #[allow(dead_code)]
156    fn eval(expr: &str) -> Value {
157        let mut ctx = Context::default();
158        register_all(&mut ctx);
159        Program::compile(expr).unwrap().execute(&ctx).unwrap()
160    }
161
162    #[test]
163    #[cfg(feature = "strings")]
164    fn test_integration_strings() {
165        assert_eq!(
166            eval("'hello'.charAt(1)"),
167            Value::String(Arc::new("e".into()))
168        );
169        assert_eq!(
170            eval("'HELLO'.lowerAscii()"),
171            Value::String(Arc::new("hello".into()))
172        );
173        assert_eq!(
174            eval("'  hello  '.trim()"),
175            Value::String(Arc::new("hello".into()))
176        );
177    }
178
179    #[test]
180    #[cfg(feature = "lists")]
181    fn test_integration_lists() {
182        assert_eq!(eval("[1, 2, 3].isSorted()"), Value::Bool(true));
183        assert_eq!(eval("[3, 1, 2].isSorted()"), Value::Bool(false));
184        assert_eq!(eval("[1, 2, 3].sum()"), Value::Int(6));
185    }
186
187    #[test]
188    #[cfg(feature = "sets")]
189    fn test_integration_sets() {
190        assert_eq!(eval("sets.contains([1, 2, 3], [1, 2])"), Value::Bool(true));
191        assert_eq!(eval("sets.intersects([1, 2], [2, 3])"), Value::Bool(true));
192    }
193
194    #[test]
195    #[cfg(feature = "regex_funcs")]
196    fn test_integration_regex() {
197        assert_eq!(
198            eval("'hello world'.find('[a-z]+')"),
199            Value::String(Arc::new("hello".into()))
200        );
201    }
202
203    #[test]
204    #[cfg(feature = "strings")]
205    fn test_dispatch_index_of_string() {
206        assert_eq!(eval("'hello world'.indexOf('world')"), Value::Int(6));
207        assert_eq!(eval("'hello'.indexOf('x')"), Value::Int(-1));
208    }
209
210    #[test]
211    #[cfg(feature = "lists")]
212    fn test_dispatch_index_of_list() {
213        assert_eq!(eval("[1, 2, 3].indexOf(2)"), Value::Int(1));
214        assert_eq!(eval("[1, 2, 3].indexOf(4)"), Value::Int(-1));
215    }
216
217    #[test]
218    #[cfg(feature = "strings")]
219    fn test_dispatch_last_index_of_string() {
220        assert_eq!(eval("'abcabc'.lastIndexOf('abc')"), Value::Int(3));
221    }
222
223    #[test]
224    #[cfg(feature = "lists")]
225    fn test_dispatch_last_index_of_list() {
226        assert_eq!(eval("[1, 2, 3, 2].lastIndexOf(2)"), Value::Int(3));
227    }
228
229    #[test]
230    #[cfg(feature = "format")]
231    fn test_integration_format() {
232        assert_eq!(
233            eval("'hello %s'.format(['world'])"),
234            Value::String(Arc::new("hello world".into()))
235        );
236        assert_eq!(
237            eval("'%d items'.format([5])"),
238            Value::String(Arc::new("5 items".into()))
239        );
240    }
241
242    #[test]
243    #[cfg(feature = "semver_funcs")]
244    fn test_integration_semver() {
245        assert_eq!(eval("isSemver('1.2.3')"), Value::Bool(true));
246        assert_eq!(eval("semver('1.2.3').major()"), Value::Int(1));
247        assert_eq!(
248            eval("semver('2.0.0').isGreaterThan(semver('1.0.0'))"),
249            Value::Bool(true)
250        );
251    }
252}