Skip to main content

ankurah_core/property/value/
json.rs

1//! JSON property type for storing structured data.
2//!
3//! The `Json` type wraps `serde_json::Value` and stores it as binary data using LWW semantics.
4//! This enables querying nested JSON fields using dot-path syntax in AnkQL:
5//!
6//! ```rust,ignore
7//! #[derive(Model)]
8//! pub struct Track {
9//!     pub name: String,
10//!     pub licensing: Json,
11//! }
12//!
13//! // Query nested fields
14//! ctx.fetch::<TrackView>("licensing.territory = ?", "US")
15//! ctx.fetch::<TrackView>("licensing.rights.holder = ?", "Label")
16//! ```
17
18use serde::{Deserialize, Serialize};
19#[cfg(feature = "wasm")]
20use wasm_bindgen::prelude::*;
21
22use crate::property::{traits::PropertyError, Property};
23use crate::value::Value;
24
25/// A JSON property type for storing structured/nested data.
26///
27/// Stores data as serialized JSON bytes using LWW (last-writer-wins) semantics.
28/// The inner `serde_json::Value` can represent any JSON structure: objects, arrays,
29/// strings, numbers, booleans, or null.
30#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
31#[serde(transparent)]
32pub struct Json(pub serde_json::Value);
33
34impl Json {
35    /// Create a new Json from a serde_json::Value.
36    pub fn new(value: serde_json::Value) -> Self { Json(value) }
37
38    /// Create a Json null value.
39    pub fn null() -> Self { Json(serde_json::Value::Null) }
40
41    /// Create a Json object from key-value pairs.
42    pub fn object(pairs: impl IntoIterator<Item = (impl Into<String>, serde_json::Value)>) -> Self {
43        let map: serde_json::Map<String, serde_json::Value> = pairs.into_iter().map(|(k, v)| (k.into(), v)).collect();
44        Json(serde_json::Value::Object(map))
45    }
46
47    /// Create a Json array.
48    pub fn array(items: impl IntoIterator<Item = serde_json::Value>) -> Self { Json(serde_json::Value::Array(items.into_iter().collect())) }
49
50    /// Get the inner serde_json::Value.
51    pub fn inner(&self) -> &serde_json::Value { &self.0 }
52
53    /// Get a mutable reference to the inner value.
54    pub fn inner_mut(&mut self) -> &mut serde_json::Value { &mut self.0 }
55
56    /// Consume self and return the inner value.
57    pub fn into_inner(self) -> serde_json::Value { self.0 }
58
59    /// Get a nested value by path (e.g., "licensing.territory").
60    ///
61    /// Returns None if the path doesn't exist or any intermediate value is not an object.
62    pub fn get_path(&self, path: &[&str]) -> Option<&serde_json::Value> {
63        let mut current = &self.0;
64        for step in path {
65            current = current.get(*step)?;
66        }
67        Some(current)
68    }
69
70    /// Check if this Json is null.
71    pub fn is_null(&self) -> bool { self.0.is_null() }
72
73    /// Check if this Json is an object.
74    pub fn is_object(&self) -> bool { self.0.is_object() }
75
76    /// Check if this Json is an array.
77    pub fn is_array(&self) -> bool { self.0.is_array() }
78}
79
80impl Default for Json {
81    fn default() -> Self { Json::null() }
82}
83
84impl From<serde_json::Value> for Json {
85    fn from(value: serde_json::Value) -> Self { Json(value) }
86}
87
88impl From<Json> for serde_json::Value {
89    fn from(json: Json) -> Self { json.0 }
90}
91
92impl std::ops::Deref for Json {
93    type Target = serde_json::Value;
94
95    fn deref(&self) -> &Self::Target { &self.0 }
96}
97
98impl std::ops::DerefMut for Json {
99    fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
100}
101
102// WASM bindings for Json type
103
104// TypeScript type definition for Json - represents any valid JSON value
105#[cfg(feature = "wasm")]
106#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)]
107const TS_JSON_TYPE: &'static str = r#"
108/** A JSON value - can be any valid JSON: object, array, string, number, boolean, or null */
109export type Json = any;
110"#;
111
112#[cfg(feature = "wasm")]
113impl From<Json> for JsValue {
114    fn from(json: Json) -> Self {
115        // Convert serde_json::Value to JsValue using serde-wasm-bindgen
116        // Use serialize_maps_as_objects to get POJOs instead of Map instances
117        let serializer = serde_wasm_bindgen::Serializer::new().serialize_maps_as_objects(true);
118        json.0.serialize(&serializer).unwrap_or(JsValue::NULL)
119    }
120}
121
122#[cfg(feature = "wasm")]
123impl wasm_bindgen::describe::WasmDescribe for Json {
124    fn describe() { JsValue::describe() }
125}
126
127#[cfg(feature = "wasm")]
128impl wasm_bindgen::convert::IntoWasmAbi for Json {
129    type Abi = <JsValue as wasm_bindgen::convert::IntoWasmAbi>::Abi;
130
131    fn into_abi(self) -> Self::Abi { JsValue::from(self).into_abi() }
132}
133
134#[cfg(feature = "wasm")]
135impl wasm_bindgen::convert::FromWasmAbi for Json {
136    type Abi = <JsValue as wasm_bindgen::convert::FromWasmAbi>::Abi;
137
138    unsafe fn from_abi(js: Self::Abi) -> Self {
139        let js_value = JsValue::from_abi(js);
140        // Convert JsValue to serde_json::Value using serde-wasm-bindgen
141        let value: serde_json::Value = serde_wasm_bindgen::from_value(js_value).unwrap_or(serde_json::Value::Null);
142        Json(value)
143    }
144}
145
146// UniFFI custom type - maps Json <-> String (JSON serialized)
147// TypeScript side can use JSON.parse/JSON.stringify via uniffi.toml config
148#[cfg(feature = "uniffi")]
149::uniffi::custom_type!(Json, String, {
150    lower: |obj| serde_json::to_string(&obj.0).expect("Failed to serialize JSON"),
151    try_lift: |val| serde_json::from_str(&val).map(Json).map_err(Into::into),
152});
153
154impl Property for Json {
155    fn into_value(&self) -> Result<Option<Value>, PropertyError> { Ok(Some(Value::Json(self.0.clone()))) }
156
157    fn from_value(value: Option<Value>) -> Result<Self, PropertyError> {
158        match value {
159            Some(Value::Json(json)) => Ok(Json(json)),
160            Some(Value::Binary(bytes)) => {
161                // Accept Binary for backwards compatibility
162                let json_value: serde_json::Value =
163                    serde_json::from_slice(&bytes).map_err(|e| PropertyError::DeserializeError(Box::new(e)))?;
164                Ok(Json(json_value))
165            }
166            Some(other) => Err(PropertyError::InvalidVariant { given: other, ty: "Json".to_string() }),
167            None => Err(PropertyError::Missing),
168        }
169    }
170}
171
172/// Macro for creating Json objects with a more ergonomic syntax.
173///
174/// # Example
175/// ```rust,ignore
176/// use ankurah_core::json;
177///
178/// let licensing = json!({
179///     "territory": "US",
180///     "rights": {
181///         "holder": "Label",
182///         "type": "exclusive"
183///     }
184/// });
185/// ```
186#[macro_export]
187macro_rules! json {
188    ($($json:tt)+) => {
189        $crate::property::value::json::Json::new(serde_json::json!($($json)+))
190    };
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[test]
198    fn test_json_roundtrip() {
199        let original = Json::object([
200            ("name".to_string(), serde_json::json!("test")),
201            ("count".to_string(), serde_json::json!(42)),
202            (
203                "nested".to_string(),
204                serde_json::json!({
205                    "inner": "value"
206                }),
207            ),
208        ]);
209
210        // Convert to Value and back
211        let value = original.into_value().unwrap().unwrap();
212        let recovered = Json::from_value(Some(value)).unwrap();
213
214        assert_eq!(original, recovered);
215    }
216
217    #[test]
218    fn test_json_get_path() {
219        let json = Json::new(serde_json::json!({
220            "licensing": {
221                "territory": "US",
222                "rights": {
223                    "holder": "Label"
224                }
225            }
226        }));
227
228        assert_eq!(json.get_path(&["licensing", "territory"]), Some(&serde_json::json!("US")));
229        assert_eq!(json.get_path(&["licensing", "rights", "holder"]), Some(&serde_json::json!("Label")));
230        assert_eq!(json.get_path(&["licensing", "nonexistent"]), None);
231        assert_eq!(json.get_path(&["nonexistent"]), None);
232    }
233
234    #[test]
235    fn test_json_null() {
236        let json = Json::null();
237        assert!(json.is_null());
238
239        let value = json.into_value().unwrap().unwrap();
240        let recovered = Json::from_value(Some(value)).unwrap();
241        assert!(recovered.is_null());
242    }
243
244    #[test]
245    fn test_json_missing() {
246        let result = Json::from_value(None);
247        assert!(matches!(result, Err(PropertyError::Missing)));
248    }
249
250    #[test]
251    fn test_json_invalid_variant() {
252        let result = Json::from_value(Some(Value::String("not json bytes".to_string())));
253        assert!(matches!(result, Err(PropertyError::InvalidVariant { .. })));
254    }
255
256    #[test]
257    fn test_json_deref() {
258        let json = Json::new(serde_json::json!({"key": "value"}));
259
260        // Can use serde_json::Value methods directly via Deref
261        assert!(json.is_object());
262        assert_eq!(json.get("key"), Some(&serde_json::json!("value")));
263    }
264}