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}