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