apollo_rust_client/namespace/
json.rs

1//! JSON namespace implementation for handling structured JSON configuration data.
2//!
3//! This module provides the `Json` struct which wraps a `serde_json::Value` and
4//! provides methods for working with JSON-formatted configuration data. It supports
5//! deserialization into custom types and maintains the original JSON structure.
6//!
7//! # Usage
8//!
9//! The `Json` struct is typically created automatically by the namespace detection
10//! system when a namespace name contains a `.json` extension, but can also be
11//! created directly from any `serde_json::Value`.
12//!
13//! # Examples
14//!
15//! ```ignore
16//! use serde_json::json;
17//! use apollo_client::namespace::json::Json;
18//!
19//! let json_data = json!({"name": "MyApp", "version": "1.0.0"});
20//! let json_namespace = Json::from(json_data);
21//! ```
22
23use log::trace;
24use serde::de::DeserializeOwned;
25
26/// Comprehensive error types that can occur when working with JSON namespaces.
27///
28/// This enum covers all possible error conditions that may arise during JSON
29/// namespace operations, from content extraction to deserialization failures.
30///
31/// # Error Categories
32///
33/// - **Content Errors**: Issues with extracting content from JSON values
34/// - **Deserialization Errors**: Problems with parsing JSON into custom types
35///
36/// # Examples
37///
38/// ```rust
39/// use apollo_rust_client::namespace::json::{Json, Error};
40/// use serde::{Deserialize, Serialize};
41/// use serde_json::json;
42///
43/// #[derive(Debug, Deserialize, Serialize)]
44/// struct MyType {
45///     name: String,
46///     value: i32,
47/// }
48///
49/// // Create a sample JSON namespace
50/// let json_data = json!({
51///     "content": r#"{"name": "test", "value": 42}"#
52/// });
53/// let json_namespace = Json::try_from(json_data).unwrap();
54///
55/// match json_namespace.to_object::<MyType>() {
56///     Ok(config) => {
57///         // Handle successful deserialization
58///         println!("Config: {:?}", config);
59///     }
60///     Err(serde_error) => {
61///         // Handle JSON parsing errors
62///         eprintln!("JSON parsing failed: {}", serde_error);
63///     }
64/// }
65/// ```
66#[derive(Debug, thiserror::Error)]
67pub enum Error {
68    /// Failed to extract content from the JSON value.
69    ///
70    /// This error occurs when the JSON value doesn't contain the expected
71    /// "content" field or when the content field is not a string.
72    #[error("Failed to get content from JSON value")]
73    ContentNotFound,
74
75    /// Failed to deserialize JSON value into the target type.
76    ///
77    /// This error occurs when the JSON content cannot be parsed into the
78    /// requested type due to format mismatches, missing fields, or type
79    /// conversion failures.
80    #[error("Failed to deserialize JSON value: {0}")]
81    DeserializeError(#[from] serde_json::Error),
82}
83
84/// A wrapper around `serde_json::Value` for JSON-formatted configuration data.
85///
86/// This struct provides a type-safe interface for working with JSON configuration
87/// data retrieved from Apollo. It maintains the original JSON structure while
88/// providing convenient methods for deserialization into custom types.
89///
90/// # Thread Safety
91///
92/// This struct is `Clone` and `Debug`, making it easy to work with in concurrent
93/// environments. The underlying `serde_json::Value` is also thread-safe.
94///
95/// # Examples
96///
97/// ```ignore
98/// use serde_json::json;
99/// use apollo_client::namespace::json::Json;
100///
101/// let json_data = json!({
102///     "database": {
103///         "host": "localhost",
104///         "port": 5432
105///     }
106/// });
107///
108/// let json_namespace = Json::from(json_data);
109/// ```
110use serde::Serialize;
111
112#[derive(Clone, Debug, Serialize)]
113pub struct Json {
114    /// The underlying JSON value containing the configuration data
115    value: serde_json::Value,
116}
117
118impl From<Json> for wasm_bindgen::JsValue {
119    fn from(val: Json) -> Self {
120        serde_wasm_bindgen::to_value(&val.value).unwrap()
121    }
122}
123
124impl Json {
125    /// Deserializes the JSON data into a custom type.
126    ///
127    /// This method attempts to deserialize the stored JSON value into any type
128    /// that implements `DeserializeOwned`. This is useful for converting the
129    /// raw JSON configuration into strongly-typed structs.
130    ///
131    /// # Type Parameters
132    ///
133    /// * `T` - The target type to deserialize into. Must implement `DeserializeOwned`.
134    ///
135    /// # Returns
136    ///
137    /// * `Ok(T)` - The deserialized configuration object
138    /// * `Err(serde_json::Error)` - If deserialization fails due to format mismatches,
139    ///   missing fields, or type conversion failures
140    ///
141    /// # Errors
142    ///
143    /// This method will return an error if:
144    /// - The JSON structure doesn't match the expected type
145    /// - Required fields are missing from the JSON
146    /// - Type conversion fails (e.g., string to number)
147    /// - The JSON is malformed or invalid
148    ///
149    /// # Examples
150    ///
151    /// ```ignore
152    /// use serde::{Deserialize, Serialize};
153    /// use serde_json::json;
154    /// use apollo_client::namespace::json::Json;
155    ///
156    /// #[derive(Deserialize, Serialize)]
157    /// struct DatabaseConfig {
158    ///     host: String,
159    ///     port: u16,
160    /// }
161    ///
162    /// let json_data = json!({
163    ///     "host": "localhost",
164    ///     "port": 5432
165    /// });
166    ///
167    /// let json_namespace = Json::from(json_data);
168    /// let config: DatabaseConfig = json_namespace.to_object()?;
169    /// ```
170    pub fn to_object<T: DeserializeOwned>(&self) -> Result<T, serde_json::Error> {
171        serde_json::from_value(self.value.clone())
172    }
173}
174
175/// Converts a `serde_json::Value` into a `Json` instance.
176///
177/// This implementation allows for easy creation of `Json` instances from
178/// raw JSON data, typically used by the namespace detection system.
179///
180/// # Arguments
181///
182/// * `json_value` - The raw JSON value containing configuration data
183///
184/// # Returns
185///
186/// * `Ok(Json)` - A new Json instance containing the parsed configuration
187/// * `Err(Error::ContentNotFound)` - If the JSON value doesn't contain a "content" field
188/// * `Err(Error::DeserializeError)` - If the content field cannot be parsed as valid JSON
189///
190/// # Errors
191///
192/// This function will return an error if:
193/// - The JSON value doesn't contain a "content" field
194/// - The "content" field is not a string
195/// - The content string cannot be parsed as valid JSON
196///
197/// # Examples
198///
199/// ```ignore
200/// use serde_json::json;
201/// use apollo_client::namespace::json::Json;
202///
203/// let json_data = json!({"content": "{\"key\": \"value\"}"});
204/// let json_namespace = Json::try_from(json_data)?;
205/// ```
206impl TryFrom<serde_json::Value> for Json {
207    type Error = crate::namespace::json::Error;
208
209    fn try_from(json_value: serde_json::Value) -> Result<Self, Self::Error> {
210        let Some(serde_json::Value::String(content_string)) = json_value.get("content") else {
211            return Err(Error::ContentNotFound);
212        };
213        trace!("content_string: {content_string:?}");
214        let value = serde_json::from_str(content_string.as_str())?;
215        trace!("value: {value:?}");
216        Ok(Self { value })
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use serde::Deserialize;
223
224    #[derive(Debug, Deserialize, PartialEq)]
225    struct TestStruct {
226        host: String,
227        port: u16,
228        run: bool,
229    }
230
231    #[cfg(not(target_arch = "wasm32"))]
232    #[tokio::test]
233    async fn test_json_to_object() {
234        crate::setup();
235        let json_namespace = crate::namespace::json::Json::try_from(serde_json::json!({
236            "content": "{\"host\": \"localhost\", \"port\": 8080, \"run\": true}"
237        }))
238        .unwrap();
239        let result: TestStruct = json_namespace.to_object().unwrap();
240        assert_eq!(
241            result,
242            TestStruct {
243                host: "localhost".to_string(),
244                port: 8080,
245                run: true,
246            }
247        );
248    }
249
250    #[cfg(not(target_arch = "wasm32"))]
251    #[tokio::test]
252    async fn test_namespace_to_object() {
253        crate::setup();
254        let namespace = crate::tests::CLIENT_NO_SECRET
255            .namespace("application.json")
256            .await
257            .unwrap();
258
259        let result = match namespace {
260            crate::namespace::Namespace::Json(json_namespace) => json_namespace.to_object(),
261            _ => panic!("Namespace is not a JSON namespace"),
262        };
263        let result: TestStruct = result.unwrap();
264        assert_eq!(
265            result,
266            TestStruct {
267                host: "localhost".to_string(),
268                port: 8080,
269                run: true,
270            }
271        );
272    }
273}