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.5", 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")] pub mod strings;
46
47#[cfg(feature = "lists")] pub mod lists;
48
49#[cfg(feature = "sets")] pub mod sets;
50
51#[cfg(feature = "regex_funcs")] pub mod regex_funcs;
52
53#[cfg(feature = "urls")] pub mod urls;
54
55#[cfg(feature = "ip")] pub mod ip;
56
57#[cfg(feature = "semver_funcs")] pub mod semver_funcs;
58
59#[cfg(feature = "format")] pub mod format;
60
61#[cfg(feature = "quantity")] pub mod quantity;
62
63#[cfg(feature = "jsonpatch")] pub mod jsonpatch;
64
65#[cfg(feature = "named_format")] pub mod named_format;
66
67#[cfg(feature = "math")] pub mod math;
68
69#[cfg(feature = "encoders")] pub mod encoders;
70
71#[cfg(feature = "validation")] pub mod escaping;
72
73#[cfg(feature = "validation")] pub mod values;
74
75#[cfg(feature = "validation")] pub mod compilation;
76
77#[cfg(feature = "validation")] pub mod validation;
78
79#[cfg(feature = "validation")] pub mod defaults;
80
81#[cfg(feature = "validation")] pub mod analysis;
82
83#[cfg(feature = "validation")] pub mod vap;
84
85mod dispatch;
86mod value_ops;
87
88/// Register all available Kubernetes CEL extension functions into the given context.
89pub fn register_all(ctx: &mut cel::Context<'_>) {
90    #[cfg(feature = "strings")]
91    strings::register(ctx);
92
93    #[cfg(feature = "lists")]
94    lists::register(ctx);
95
96    #[cfg(feature = "sets")]
97    sets::register(ctx);
98
99    #[cfg(feature = "regex_funcs")]
100    regex_funcs::register(ctx);
101
102    #[cfg(feature = "urls")]
103    urls::register(ctx);
104
105    #[cfg(feature = "ip")]
106    ip::register(ctx);
107
108    #[cfg(feature = "semver_funcs")]
109    semver_funcs::register(ctx);
110
111    #[cfg(feature = "format")]
112    format::register(ctx);
113
114    #[cfg(feature = "quantity")]
115    quantity::register(ctx);
116
117    #[cfg(feature = "jsonpatch")]
118    jsonpatch::register(ctx);
119
120    #[cfg(feature = "named_format")]
121    named_format::register(ctx);
122
123    #[cfg(feature = "math")]
124    math::register(ctx);
125
126    #[cfg(feature = "encoders")]
127    encoders::register(ctx);
128
129    // Dispatch: registers functions with name collisions (indexOf, reverse,
130    // min/max, string, ip, isGreaterThan, etc.). Order-independent since
131    // individual modules no longer register these conflicting names.
132    dispatch::register(ctx);
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[allow(unused_imports)] use std::sync::Arc;
140
141    use cel::{Context, Program, Value};
142
143    #[allow(dead_code)]
144    fn eval(expr: &str) -> Value {
145        let mut ctx = Context::default();
146        register_all(&mut ctx);
147        Program::compile(expr).unwrap().execute(&ctx).unwrap()
148    }
149
150    #[test]
151    #[cfg(feature = "strings")]
152    fn test_integration_strings() {
153        assert_eq!(eval("'hello'.charAt(1)"), Value::String(Arc::new("e".into())));
154        assert_eq!(
155            eval("'HELLO'.lowerAscii()"),
156            Value::String(Arc::new("hello".into()))
157        );
158        assert_eq!(
159            eval("'  hello  '.trim()"),
160            Value::String(Arc::new("hello".into()))
161        );
162    }
163
164    #[test]
165    #[cfg(feature = "lists")]
166    fn test_integration_lists() {
167        assert_eq!(eval("[1, 2, 3].isSorted()"), Value::Bool(true));
168        assert_eq!(eval("[3, 1, 2].isSorted()"), Value::Bool(false));
169        assert_eq!(eval("[1, 2, 3].sum()"), Value::Int(6));
170    }
171
172    #[test]
173    #[cfg(feature = "sets")]
174    fn test_integration_sets() {
175        assert_eq!(eval("sets.contains([1, 2, 3], [1, 2])"), Value::Bool(true));
176        assert_eq!(eval("sets.intersects([1, 2], [2, 3])"), Value::Bool(true));
177    }
178
179    #[test]
180    #[cfg(feature = "regex_funcs")]
181    fn test_integration_regex() {
182        assert_eq!(
183            eval("'hello world'.find('[a-z]+')"),
184            Value::String(Arc::new("hello".into()))
185        );
186    }
187
188    #[test]
189    #[cfg(feature = "strings")]
190    fn test_dispatch_index_of_string() {
191        assert_eq!(eval("'hello world'.indexOf('world')"), Value::Int(6));
192        assert_eq!(eval("'hello'.indexOf('x')"), Value::Int(-1));
193    }
194
195    #[test]
196    #[cfg(feature = "lists")]
197    fn test_dispatch_index_of_list() {
198        assert_eq!(eval("[1, 2, 3].indexOf(2)"), Value::Int(1));
199        assert_eq!(eval("[1, 2, 3].indexOf(4)"), Value::Int(-1));
200    }
201
202    #[test]
203    #[cfg(feature = "strings")]
204    fn test_dispatch_last_index_of_string() {
205        assert_eq!(eval("'abcabc'.lastIndexOf('abc')"), Value::Int(3));
206    }
207
208    #[test]
209    #[cfg(feature = "lists")]
210    fn test_dispatch_last_index_of_list() {
211        assert_eq!(eval("[1, 2, 3, 2].lastIndexOf(2)"), Value::Int(3));
212    }
213
214    #[test]
215    #[cfg(feature = "format")]
216    fn test_integration_format() {
217        assert_eq!(
218            eval("'hello %s'.format(['world'])"),
219            Value::String(Arc::new("hello world".into()))
220        );
221        assert_eq!(
222            eval("'%d items'.format([5])"),
223            Value::String(Arc::new("5 items".into()))
224        );
225    }
226
227    #[test]
228    #[cfg(feature = "semver_funcs")]
229    fn test_integration_semver() {
230        assert_eq!(eval("isSemver('1.2.3')"), Value::Bool(true));
231        assert_eq!(eval("semver('1.2.3').major()"), Value::Int(1));
232        assert_eq!(
233            eval("semver('2.0.0').isGreaterThan(semver('1.0.0'))"),
234            Value::Bool(true)
235        );
236    }
237}