Skip to main content

robinpath_modules/modules/
cookie_mod.rs

1use robinpath::{RobinPath, Value};
2
3pub fn register(rp: &mut RobinPath) {
4    // cookie.parse(cookieString) -> Object with key-value pairs
5    rp.register_builtin("cookie.parse", |args, _| {
6        let cookie_str = args.first().map(|v| v.to_display_string()).unwrap_or_default();
7        let mut obj = indexmap::IndexMap::new();
8        for pair in cookie_str.split(';') {
9            let pair = pair.trim();
10            if pair.is_empty() {
11                continue;
12            }
13            if let Some((key, value)) = pair.split_once('=') {
14                obj.insert(
15                    key.trim().to_string(),
16                    Value::String(value.trim().to_string()),
17                );
18            }
19        }
20        Ok(Value::Object(obj))
21    });
22
23    // cookie.stringify(obj) -> "key1=val1; key2=val2"
24    rp.register_builtin("cookie.stringify", |args, _| {
25        let obj = args.first().cloned().unwrap_or(Value::Null);
26        match obj {
27            Value::Object(map) => {
28                let parts: Vec<String> = map
29                    .iter()
30                    .map(|(k, v)| format!("{}={}", k, v.to_display_string()))
31                    .collect();
32                Ok(Value::String(parts.join("; ")))
33            }
34            _ => Err("cookie.stringify expects an object".to_string()),
35        }
36    });
37
38    // cookie.get(cookieString, name) -> value or null
39    rp.register_builtin("cookie.get", |args, _| {
40        let cookie_str = args.first().map(|v| v.to_display_string()).unwrap_or_default();
41        let name = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
42        for pair in cookie_str.split(';') {
43            let pair = pair.trim();
44            if let Some((key, value)) = pair.split_once('=') {
45                if key.trim() == name {
46                    return Ok(Value::String(value.trim().to_string()));
47                }
48            }
49        }
50        Ok(Value::Null)
51    });
52
53    // cookie.set(cookieString, name, value, options?) -> updated cookie string
54    rp.register_builtin("cookie.set", |args, _| {
55        let cookie_str = args.first().map(|v| v.to_display_string()).unwrap_or_default();
56        let name = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
57        let value = args.get(2).map(|v| v.to_display_string()).unwrap_or_default();
58        let opts = args.get(3).cloned().unwrap_or(Value::Null);
59
60        // Parse existing cookies, replace or add
61        let mut pairs: Vec<(String, String)> = Vec::new();
62        let mut found = false;
63        for pair in cookie_str.split(';') {
64            let pair = pair.trim();
65            if pair.is_empty() {
66                continue;
67            }
68            if let Some((key, val)) = pair.split_once('=') {
69                let key = key.trim().to_string();
70                if key == name {
71                    pairs.push((key, value.clone()));
72                    found = true;
73                } else {
74                    pairs.push((key, val.trim().to_string()));
75                }
76            }
77        }
78        if !found {
79            pairs.push((name.clone(), value.clone()));
80        }
81
82        // Build base cookie string
83        let base: Vec<String> = pairs.iter().map(|(k, v)| format!("{}={}", k, v)).collect();
84        let mut result = base.join("; ");
85
86        // Append options as Set-Cookie attributes
87        if let Value::Object(obj) = &opts {
88            if let Some(path) = obj.get("path") {
89                result.push_str(&format!("; Path={}", path.to_display_string()));
90            }
91            if let Some(domain) = obj.get("domain") {
92                result.push_str(&format!("; Domain={}", domain.to_display_string()));
93            }
94            if let Some(max_age) = obj.get("maxAge") {
95                result.push_str(&format!("; Max-Age={}", max_age.to_number() as i64));
96            }
97            if let Some(secure) = obj.get("secure") {
98                if secure.is_truthy() {
99                    result.push_str("; Secure");
100                }
101            }
102            if let Some(http_only) = obj.get("httpOnly") {
103                if http_only.is_truthy() {
104                    result.push_str("; HttpOnly");
105                }
106            }
107            if let Some(same_site) = obj.get("sameSite") {
108                result.push_str(&format!("; SameSite={}", same_site.to_display_string()));
109            }
110        }
111
112        Ok(Value::String(result))
113    });
114
115    // cookie.delete(cookieString, name) -> updated string without that cookie
116    rp.register_builtin("cookie.delete", |args, _| {
117        let cookie_str = args.first().map(|v| v.to_display_string()).unwrap_or_default();
118        let name = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
119        let mut pairs: Vec<String> = Vec::new();
120        for pair in cookie_str.split(';') {
121            let pair = pair.trim();
122            if pair.is_empty() {
123                continue;
124            }
125            if let Some((key, val)) = pair.split_once('=') {
126                if key.trim() != name {
127                    pairs.push(format!("{}={}", key.trim(), val.trim()));
128                }
129            }
130        }
131        Ok(Value::String(pairs.join("; ")))
132    });
133
134    // cookie.isExpired(cookieString) -> bool
135    rp.register_builtin("cookie.isExpired", |args, _| {
136        let cookie_str = args.first().map(|v| v.to_display_string()).unwrap_or_default();
137        let lower = cookie_str.to_lowercase();
138
139        // Check for max-age=0
140        for pair in lower.split(';') {
141            let pair = pair.trim();
142            if let Some((key, val)) = pair.split_once('=') {
143                let key = key.trim();
144                let val = val.trim();
145                if key == "max-age" {
146                    if let Ok(age) = val.parse::<i64>() {
147                        if age <= 0 {
148                            return Ok(Value::Bool(true));
149                        }
150                    }
151                }
152                if key == "expires" {
153                    // Try to detect "Thu, 01 Jan 1970" style expired dates
154                    if val.contains("1970") || val.contains("Thu, 01 Jan 1970") {
155                        return Ok(Value::Bool(true));
156                    }
157                }
158            }
159        }
160
161        Ok(Value::Bool(false))
162    });
163}