dbrest_core/config/
jwt.rs1use compact_str::CompactString;
6
7use super::error::ConfigError;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum JsPathExp {
19 Key(CompactString),
21 Index(usize),
23}
24
25impl JsPathExp {
26 pub fn is_key(&self) -> bool {
28 matches!(self, JsPathExp::Key(_))
29 }
30
31 pub fn is_index(&self) -> bool {
33 matches!(self, JsPathExp::Index(_))
34 }
35
36 pub fn as_key(&self) -> Option<&str> {
38 match self {
39 JsPathExp::Key(k) => Some(k.as_str()),
40 JsPathExp::Index(_) => None,
41 }
42 }
43
44 pub fn as_index(&self) -> Option<usize> {
46 match self {
47 JsPathExp::Key(_) => None,
48 JsPathExp::Index(i) => Some(*i),
49 }
50 }
51}
52
53pub fn parse_js_path(input: &str) -> Result<Vec<JsPathExp>, ConfigError> {
70 let mut result = Vec::new();
71 let mut chars = input.chars().peekable();
72
73 while let Some(&c) = chars.peek() {
74 match c {
75 '.' => {
76 chars.next(); let key = parse_key(&mut chars);
78 if !key.is_empty() {
79 result.push(JsPathExp::Key(key.into()));
80 }
81 }
82 '[' => {
83 chars.next(); let index = parse_index(&mut chars)?;
85 result.push(JsPathExp::Index(index));
86 }
87 _ => {
88 let key = parse_key(&mut chars);
90 if !key.is_empty() {
91 result.push(JsPathExp::Key(key.into()));
92 }
93 }
94 }
95 }
96
97 if result.is_empty() {
98 result.push(JsPathExp::Key("role".into()));
100 }
101
102 Ok(result)
103}
104
105fn parse_key(chars: &mut std::iter::Peekable<std::str::Chars>) -> String {
107 let mut key = String::new();
108 while let Some(&c) = chars.peek() {
109 if c == '.' || c == '[' {
110 break;
111 }
112 key.push(c);
113 chars.next();
114 }
115 key
116}
117
118fn parse_index(chars: &mut std::iter::Peekable<std::str::Chars>) -> Result<usize, ConfigError> {
120 let mut num = String::new();
121
122 while let Some(&c) = chars.peek() {
123 if c == ']' {
124 chars.next(); break;
126 }
127 if !c.is_ascii_digit() {
128 return Err(ConfigError::InvalidJsPath(format!(
129 "Invalid character '{}' in array index",
130 c
131 )));
132 }
133 num.push(c);
134 chars.next();
135 }
136
137 if num.is_empty() {
138 return Err(ConfigError::InvalidJsPath("Empty array index".to_string()));
139 }
140
141 num.parse()
142 .map_err(|_| ConfigError::InvalidJsPath(format!("Invalid array index: {}", num)))
143}
144
145pub fn extract_from_json<'a>(
169 value: &'a serde_json::Value,
170 path: &[JsPathExp],
171) -> Option<&'a serde_json::Value> {
172 let mut current = value;
173
174 for exp in path {
175 current = match exp {
176 JsPathExp::Key(key) => current.get(key.as_str())?,
177 JsPathExp::Index(idx) => current.get(*idx)?,
178 };
179 }
180
181 Some(current)
182}
183
184pub fn extract_string_from_json(value: &serde_json::Value, path: &[JsPathExp]) -> Option<String> {
186 let extracted = extract_from_json(value, path)?;
187
188 match extracted {
189 serde_json::Value::String(s) => Some(s.clone()),
190 serde_json::Value::Number(n) => Some(n.to_string()),
192 serde_json::Value::Bool(b) => Some(b.to_string()),
193 _ => None,
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200 use serde_json::json;
201
202 #[test]
203 fn test_parse_simple_key() {
204 let path = parse_js_path(".role").unwrap();
205 assert_eq!(path, vec![JsPathExp::Key("role".into())]);
206 }
207
208 #[test]
209 fn test_parse_nested_keys() {
210 let path = parse_js_path(".realm_access.roles").unwrap();
211 assert_eq!(
212 path,
213 vec![
214 JsPathExp::Key("realm_access".into()),
215 JsPathExp::Key("roles".into()),
216 ]
217 );
218 }
219
220 #[test]
221 fn test_parse_with_index() {
222 let path = parse_js_path(".realm_access.roles[0]").unwrap();
223 assert_eq!(
224 path,
225 vec![
226 JsPathExp::Key("realm_access".into()),
227 JsPathExp::Key("roles".into()),
228 JsPathExp::Index(0),
229 ]
230 );
231 }
232
233 #[test]
234 fn test_parse_multiple_indices() {
235 let path = parse_js_path(".data[0][1]").unwrap();
236 assert_eq!(
237 path,
238 vec![
239 JsPathExp::Key("data".into()),
240 JsPathExp::Index(0),
241 JsPathExp::Index(1),
242 ]
243 );
244 }
245
246 #[test]
247 fn test_parse_without_leading_dot() {
248 let path = parse_js_path("role").unwrap();
249 assert_eq!(path, vec![JsPathExp::Key("role".into())]);
250 }
251
252 #[test]
253 fn test_parse_empty_defaults_to_role() {
254 let path = parse_js_path("").unwrap();
255 assert_eq!(path, vec![JsPathExp::Key("role".into())]);
256 }
257
258 #[test]
259 fn test_parse_invalid_index() {
260 let result = parse_js_path(".roles[abc]");
261 assert!(result.is_err());
262 }
263
264 #[test]
265 fn test_extract_simple() {
266 let data = json!({ "role": "admin" });
267 let path = parse_js_path(".role").unwrap();
268 let value = extract_from_json(&data, &path);
269 assert_eq!(value.and_then(|v| v.as_str()), Some("admin"));
270 }
271
272 #[test]
273 fn test_extract_nested() {
274 let data = json!({
275 "realm_access": {
276 "roles": ["user", "admin"]
277 }
278 });
279 let path = parse_js_path(".realm_access.roles[1]").unwrap();
280 let value = extract_from_json(&data, &path);
281 assert_eq!(value.and_then(|v| v.as_str()), Some("admin"));
282 }
283
284 #[test]
285 fn test_extract_missing() {
286 let data = json!({ "role": "admin" });
287 let path = parse_js_path(".missing.path").unwrap();
288 let value = extract_from_json(&data, &path);
289 assert!(value.is_none());
290 }
291
292 #[test]
293 fn test_extract_string_from_number() {
294 let data = json!({ "id": 123 });
295 let path = parse_js_path(".id").unwrap();
296 let value = extract_string_from_json(&data, &path);
297 assert_eq!(value, Some("123".to_string()));
298 }
299
300 #[test]
301 fn test_js_path_exp_methods() {
302 let key = JsPathExp::Key("test".into());
303 assert!(key.is_key());
304 assert!(!key.is_index());
305 assert_eq!(key.as_key(), Some("test"));
306 assert_eq!(key.as_index(), None);
307
308 let idx = JsPathExp::Index(5);
309 assert!(!idx.is_key());
310 assert!(idx.is_index());
311 assert_eq!(idx.as_key(), None);
312 assert_eq!(idx.as_index(), Some(5));
313 }
314}