Skip to main content

kube_cel/
jsonpatch.rs

1//! JSONPatch key escaping for Kubernetes CEL.
2//!
3//! Provides `jsonpatch.escapeKey()` to escape JSONPatch path keys
4//! per RFC 6901 (`~` → `~0`, `/` → `~1`).
5
6use cel::objects::Value;
7use cel::{Context, ResolveResult};
8use std::sync::Arc;
9
10/// Register the jsonpatch extension functions.
11pub fn register(ctx: &mut Context<'_>) {
12    ctx.add_function("jsonpatch.escapeKey", escape_key);
13}
14
15/// `jsonpatch.escapeKey(<string>) -> string`
16///
17/// Escapes a string for use as a JSONPatch path key per RFC 6901.
18/// `~` is replaced with `~0` first, then `/` is replaced with `~1`.
19fn escape_key(s: Arc<String>) -> ResolveResult {
20    let escaped = s.replace('~', "~0").replace('/', "~1");
21    Ok(Value::String(Arc::new(escaped)))
22}
23
24#[cfg(test)]
25mod tests {
26    use super::*;
27    use cel::Program;
28
29    fn eval(expr: &str) -> Value {
30        let mut ctx = Context::default();
31        register(&mut ctx);
32        Program::compile(expr).unwrap().execute(&ctx).unwrap()
33    }
34
35    fn eval_str(expr: &str) -> String {
36        match eval(expr) {
37            Value::String(s) => (*s).clone(),
38            other => panic!("expected string, got {other:?}"),
39        }
40    }
41
42    #[test]
43    fn test_escape_tilde_and_slash() {
44        assert_eq!(
45            eval_str("jsonpatch.escapeKey('k8s.io/my~label')"),
46            "k8s.io~1my~0label"
47        );
48    }
49
50    #[test]
51    fn test_escape_tilde_only() {
52        assert_eq!(eval_str("jsonpatch.escapeKey('a~b')"), "a~0b");
53    }
54
55    #[test]
56    fn test_escape_slash_only() {
57        assert_eq!(eval_str("jsonpatch.escapeKey('a/b')"), "a~1b");
58    }
59
60    #[test]
61    fn test_escape_no_special_chars() {
62        assert_eq!(eval_str("jsonpatch.escapeKey('hello')"), "hello");
63    }
64
65    #[test]
66    fn test_escape_empty_string() {
67        assert_eq!(eval_str("jsonpatch.escapeKey('')"), "");
68    }
69
70    #[test]
71    fn test_escape_multiple() {
72        assert_eq!(eval_str("jsonpatch.escapeKey('~/~/')"), "~0~1~0~1");
73    }
74
75    #[test]
76    fn test_escape_order_matters() {
77        // ~ must be escaped before / to avoid double-escaping
78        // Input: ~1 → should become ~01 (escape ~ to ~0, then 1 stays)
79        assert_eq!(eval_str("jsonpatch.escapeKey('~1')"), "~01");
80    }
81}