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