Skip to main content

rok_utils/types/
mod.rs

1//! Runtime type guards and JSON path utilities.
2//!
3//! This module provides type checking and dot-path access for JSON values,
4//! inspired by AdonisJS `@adonisjs/core/helpers`.
5//!
6//! Requires the `json` feature flag.
7//!
8//! # Example
9//!
10//! ```rust
11//! use rok_utils::types::{is_string, is_number, get_path};
12//! use serde_json::json;
13//!
14//! let value = json!({"Name": "Alice", "age": 30});
15//!
16//! assert!(is_string(&value["Name"]));
17//! assert!(is_number(&value["age"]));
18//!
19//! let data = json!({"user": {"address": {"city": "NYC"}}});
20//! let city = get_path(&data, "user.address.city");
21//! assert_eq!(city, Some(&json!("NYC")));
22//! ```
23
24#[cfg(feature = "json")]
25use serde_json::Value;
26
27#[cfg(feature = "json")]
28pub fn is_string(val: &Value) -> bool {
29    val.is_string()
30}
31
32#[cfg(feature = "json")]
33pub fn is_number(val: &Value) -> bool {
34    val.is_number()
35}
36
37#[cfg(feature = "json")]
38pub fn is_bool(val: &Value) -> bool {
39    val.is_boolean()
40}
41
42#[cfg(feature = "json")]
43pub fn is_array(val: &Value) -> bool {
44    val.is_array()
45}
46
47#[cfg(feature = "json")]
48pub fn is_object(val: &Value) -> bool {
49    val.is_object()
50}
51
52#[cfg(feature = "json")]
53pub fn is_null(val: &Value) -> bool {
54    val.is_null()
55}
56
57#[cfg(feature = "json")]
58pub fn is_defined(val: &Value) -> bool {
59    !val.is_null()
60}
61
62#[cfg(feature = "json")]
63pub fn deep_equal(a: &Value, b: &Value) -> bool {
64    match (a, b) {
65        (Value::Null, Value::Null) => true,
66        (Value::Bool(a), Value::Bool(b)) => a == b,
67        (Value::Number(a), Value::Number(b)) => a == b,
68        (Value::String(a), Value::String(b)) => a == b,
69        (Value::Array(a), Value::Array(b)) => {
70            a.len() == b.len() && a.iter().zip(b.iter()).all(|(x, y)| deep_equal(x, y))
71        }
72        (Value::Object(a), Value::Object(b)) => {
73            a.len() == b.len()
74                && a.iter()
75                    .all(|(k, v)| b.get(k).map(|bv| deep_equal(v, bv)).unwrap_or(false))
76        }
77        _ => false,
78    }
79}
80
81#[cfg(feature = "json")]
82pub fn get_path<'a>(val: &'a Value, path: &str) -> Option<&'a Value> {
83    let mut current: Option<&'a Value> = Some(val);
84    for key in path.split('.') {
85        current = current.and_then(|v| {
86            if let Some(obj) = v.as_object() {
87                obj.get(key)
88            } else if let Some(arr) = v.as_array() {
89                key.parse::<usize>().ok().and_then(|i| arr.get(i))
90            } else {
91                None
92            }
93        });
94    }
95    current
96}
97
98#[cfg(feature = "json")]
99pub fn set_path(val: Value, path: &str, new: Value) -> Value {
100    let keys: Vec<&str> = path.split('.').collect();
101    if keys.is_empty() {
102        return new;
103    }
104
105    let mut result = val;
106    if let Some(_last) = keys.last() {
107        for key in &keys[..keys.len() - 1] {
108            if result.get(*key).is_none() {
109                if let Ok(idx) = key.parse::<usize>() {
110                    while result.as_array().map(|a| a.len()).unwrap_or(0) <= idx {
111                        result = Value::Array(vec![result, Value::Null]);
112                    }
113                } else {
114                    result = Value::Object(serde_json::Map::new());
115                }
116            }
117        }
118    }
119
120    if let Value::Object(ref mut obj) = result {
121        if let Some(last_key) = keys.last() {
122            obj.insert(last_key.to_string(), new);
123        }
124    }
125    result
126}
127
128#[cfg(feature = "json")]
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use serde_json::json;
133
134    #[test]
135    fn test_is_string() {
136        assert!(is_string(&json!("hello")));
137        assert!(!is_string(&json!(42)));
138    }
139
140    #[test]
141    fn test_is_number() {
142        assert!(is_number(&json!(42)));
143        assert!(!is_number(&json!("hello")));
144    }
145
146    #[test]
147    fn test_is_bool() {
148        assert!(is_bool(&json!(true)));
149        assert!(!is_bool(&json!(42)));
150    }
151
152    #[test]
153    fn test_is_array() {
154        assert!(is_array(&json!([1, 2, 3])));
155        assert!(!is_array(&json!(42)));
156    }
157
158    #[test]
159    fn test_is_object() {
160        assert!(is_object(&json!({"a": 1})));
161        assert!(!is_object(&json!([1, 2])));
162    }
163
164    #[test]
165    fn test_is_null() {
166        assert!(is_null(&json!(null)));
167        assert!(!is_null(&json!(42)));
168    }
169
170    #[test]
171    fn test_deep_equal() {
172        assert!(deep_equal(&json!({"a": 1}), &json!({"a": 1})));
173        assert!(!deep_equal(&json!({"a": 1}), &json!({"a": 2})));
174    }
175
176    #[test]
177    fn test_get_path() {
178        let val = json!({"user": {"address": {"city": "NYC"}}});
179        assert_eq!(get_path(&val, "user.address.city"), Some(&json!("NYC")));
180    }
181
182    #[test]
183    fn test_set_path() {
184        let val = json!({"a": 1});
185        let result = set_path(val, "b", json!(2));
186        assert_eq!(result.get("b"), Some(&json!(2)));
187    }
188}