kibana_object_manager/transform/
field_escaper.rs1use crate::etl::Transformer;
7use eyre::{Context, Result};
8use serde_json::Value;
9
10pub struct FieldEscaper {
32 fields: Vec<String>,
33}
34
35impl FieldEscaper {
36 pub fn new(fields: Vec<&str>) -> Self {
38 Self {
39 fields: fields.iter().map(|s| s.to_string()).collect(),
40 }
41 }
42
43 pub fn default_kibana_fields() -> Self {
45 Self::new(vec![
46 "attributes.panelsJSON",
47 "attributes.fieldFormatMap",
48 "attributes.controlGroupInput.ignoreParentSettingsJSON",
49 "attributes.controlGroupInput.panelsJSON",
50 "attributes.kibanaSavedObjectMeta.searchSourceJSON",
51 "attributes.optionsJSON",
52 "attributes.visState",
53 "attributes.fieldAttrs",
54 ])
55 }
56
57 fn get_nested_mut<'a>(obj: &'a mut Value, path: &str) -> Option<&'a mut Value> {
58 let parts: Vec<&str> = path.split('.').collect();
59 let mut current = obj;
60
61 for part in parts {
62 current = current.get_mut(part)?;
63 }
64
65 Some(current)
66 }
67}
68
69impl Transformer for FieldEscaper {
70 type Input = Value;
71 type Output = Value;
72
73 fn transform(&self, mut input: Self::Input) -> Result<Self::Output> {
74 for field_path in &self.fields {
75 if let Some(field) = Self::get_nested_mut(&mut input, field_path) {
76 if field.is_object() || field.is_array() {
78 let json_string = serde_json::to_string(field)
79 .with_context(|| format!("Failed to escape field: {}", field_path))?;
80 *field = Value::String(json_string);
81 }
82 }
83 }
84 Ok(input)
85 }
86}
87
88pub struct FieldUnescaper {
111 fields: Vec<String>,
112}
113
114impl FieldUnescaper {
115 pub fn new(fields: Vec<&str>) -> Self {
117 Self {
118 fields: fields.iter().map(|s| s.to_string()).collect(),
119 }
120 }
121
122 pub fn default_kibana_fields() -> Self {
124 Self::new(vec![
125 "attributes.panelsJSON",
126 "attributes.fieldFormatMap",
127 "attributes.controlGroupInput.ignoreParentSettingsJSON",
128 "attributes.controlGroupInput.panelsJSON",
129 "attributes.kibanaSavedObjectMeta.searchSourceJSON",
130 "attributes.optionsJSON",
131 "attributes.visState",
132 "attributes.fieldAttrs",
133 ])
134 }
135
136 fn get_nested_mut<'a>(obj: &'a mut Value, path: &str) -> Option<&'a mut Value> {
137 let parts: Vec<&str> = path.split('.').collect();
138 let mut current = obj;
139
140 for part in parts {
141 current = current.get_mut(part)?;
142 }
143
144 Some(current)
145 }
146}
147
148impl Transformer for FieldUnescaper {
149 type Input = Value;
150 type Output = Value;
151
152 fn transform(&self, mut input: Self::Input) -> Result<Self::Output> {
153 for field_path in &self.fields {
154 if let Some(field) = Self::get_nested_mut(&mut input, field_path) {
155 if let Some(json_str) = field.as_str() {
157 let trimmed = json_str.trim();
159 if trimmed.starts_with('{') || trimmed.starts_with('[') {
160 match serde_json::from_str(json_str) {
161 Ok(parsed) => *field = parsed,
162 Err(_) => {
163 log::debug!(
165 "Failed to unescape field {}, leaving as string",
166 field_path
167 );
168 }
169 }
170 }
171 }
172 }
173 }
174 Ok(input)
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181 use serde_json::json;
182
183 #[test]
184 fn test_escape_field() {
185 let escaper = FieldEscaper::new(vec!["attributes.visState"]);
186 let input = json!({
187 "attributes": {
188 "visState": {"type": "pie", "params": {"size": 10}},
189 "title": "My Viz"
190 }
191 });
192
193 let output = escaper.transform(input).unwrap();
194
195 assert!(output["attributes"]["visState"].is_string());
196 let vis_state_str = output["attributes"]["visState"].as_str().unwrap();
197 let parsed: Value = serde_json::from_str(vis_state_str).unwrap();
198 assert_eq!(parsed["type"], "pie");
199 assert_eq!(parsed["params"]["size"], 10);
200 }
201
202 #[test]
203 fn test_unescape_field() {
204 let unescaper = FieldUnescaper::new(vec!["attributes.visState"]);
205 let input = json!({
206 "attributes": {
207 "visState": r#"{"type":"pie","params":{"size":10}}"#,
208 "title": "My Viz"
209 }
210 });
211
212 let output = unescaper.transform(input).unwrap();
213
214 assert!(output["attributes"]["visState"].is_object());
215 assert_eq!(output["attributes"]["visState"]["type"], "pie");
216 assert_eq!(output["attributes"]["visState"]["params"]["size"], 10);
217 }
218
219 #[test]
220 fn test_roundtrip() {
221 let unescaper = FieldUnescaper::new(vec!["attributes.visState"]);
222 let escaper = FieldEscaper::new(vec!["attributes.visState"]);
223
224 let original = json!({
225 "attributes": {
226 "visState": r#"{"type":"pie"}"#
227 }
228 });
229
230 let unescaped = unescaper.transform(original.clone()).unwrap();
232 assert!(unescaped["attributes"]["visState"].is_object());
233
234 let escaped = escaper.transform(unescaped).unwrap();
236 assert!(escaped["attributes"]["visState"].is_string());
237
238 let escaped_str = escaped["attributes"]["visState"].as_str().unwrap();
240 let original_str = original["attributes"]["visState"].as_str().unwrap();
241 let escaped_parsed: Value = serde_json::from_str(escaped_str).unwrap();
242 let original_parsed: Value = serde_json::from_str(original_str).unwrap();
243 assert_eq!(escaped_parsed, original_parsed);
244 }
245
246 #[test]
247 fn test_nested_path() {
248 let escaper = FieldEscaper::new(vec!["attributes.controlGroupInput.panelsJSON"]);
249 let input = json!({
250 "attributes": {
251 "controlGroupInput": {
252 "panelsJSON": {"panel1": {"id": "test"}}
253 }
254 }
255 });
256
257 let output = escaper.transform(input).unwrap();
258 assert!(output["attributes"]["controlGroupInput"]["panelsJSON"].is_string());
259 }
260
261 #[test]
262 fn test_missing_field_ignored() {
263 let escaper = FieldEscaper::new(vec!["attributes.nonexistent"]);
264 let input = json!({"attributes": {"title": "Test"}});
265
266 let output = escaper.transform(input.clone()).unwrap();
267 assert_eq!(output, input);
268 }
269
270 #[test]
271 fn test_already_string_unchanged() {
272 let escaper = FieldEscaper::new(vec!["attributes.title"]);
273 let input = json!({"attributes": {"title": "Already a string"}});
274
275 let output = escaper.transform(input.clone()).unwrap();
276 assert_eq!(output, input);
277 }
278
279 #[test]
280 fn test_invalid_json_string_unchanged() {
281 let unescaper = FieldUnescaper::new(vec!["attributes.visState"]);
282 let input = json!({
283 "attributes": {
284 "visState": "not valid json"
285 }
286 });
287
288 let output = unescaper.transform(input.clone()).unwrap();
289 assert_eq!(output, input);
291 }
292}