omni_dev/data/
yaml.rs

1//! YAML processing utilities
2
3use anyhow::{Context, Result};
4use serde::{Deserialize, Serialize};
5use std::fs;
6use std::path::Path;
7use yaml_rust_davvid::YamlEmitter;
8
9/// Serialize data structure to YAML string with proper multi-line formatting
10pub fn to_yaml<T: Serialize>(data: &T) -> Result<String> {
11    use tracing::debug;
12
13    debug!("Starting YAML serialization with hybrid approach");
14
15    // First convert to serde_yaml::Value, then to yaml-rust format
16    let serde_value = serde_yaml::to_value(data).context("Failed to serialize to serde value")?;
17    debug!("Converted to serde_yaml::Value successfully");
18
19    let yaml_rust_value = convert_serde_to_yaml_rust(&serde_value)?;
20    debug!("Converted to yaml-rust format successfully");
21
22    // Use yaml-rust emitter with multiline strings enabled
23    let mut output = String::new();
24    let mut emitter = YamlEmitter::new(&mut output);
25    emitter.multiline_strings(true);
26    debug!("Created YamlEmitter with multiline_strings(true)");
27
28    emitter
29        .dump(&yaml_rust_value)
30        .context("Failed to emit YAML")?;
31
32    debug!(
33        output_length = output.len(),
34        output_preview = %output.lines().take(10).collect::<Vec<_>>().join("\\n"),
35        "YAML serialization completed"
36    );
37
38    Ok(output)
39}
40
41/// Convert serde_yaml::Value to yaml_rust_davvid::Yaml
42fn convert_serde_to_yaml_rust(value: &serde_yaml::Value) -> Result<yaml_rust_davvid::Yaml> {
43    use tracing::debug;
44    use yaml_rust_davvid::Yaml;
45
46    match value {
47        serde_yaml::Value::Null => Ok(Yaml::Null),
48        serde_yaml::Value::Bool(b) => Ok(Yaml::Boolean(*b)),
49        serde_yaml::Value::Number(n) => {
50            if let Some(i) = n.as_i64() {
51                Ok(Yaml::Integer(i))
52            } else if let Some(f) = n.as_f64() {
53                Ok(Yaml::Real(f.to_string()))
54            } else {
55                Ok(Yaml::String(n.to_string()))
56            }
57        }
58        serde_yaml::Value::String(s) => {
59            debug!(
60                string_length = s.len(),
61                string_preview = %s.lines().take(3).collect::<Vec<_>>().join("\\n"),
62                "Converting string value to yaml-rust"
63            );
64            // For now, just convert normally - yaml-rust will make formatting decisions
65            Ok(Yaml::String(s.clone()))
66        }
67        serde_yaml::Value::Sequence(seq) => {
68            let yaml_seq: Result<Vec<_>> = seq.iter().map(convert_serde_to_yaml_rust).collect();
69            Ok(Yaml::Array(yaml_seq?))
70        }
71        serde_yaml::Value::Mapping(map) => {
72            let mut yaml_map = yaml_rust_davvid::yaml::Hash::new();
73            for (k, v) in map {
74                let yaml_key = convert_serde_to_yaml_rust(k)?;
75                let yaml_value = convert_serde_to_yaml_rust(v)?;
76                yaml_map.insert(yaml_key, yaml_value);
77            }
78            Ok(Yaml::Hash(yaml_map))
79        }
80        serde_yaml::Value::Tagged(tagged) => {
81            // Handle tagged values by converting the inner value
82            convert_serde_to_yaml_rust(&tagged.value)
83        }
84    }
85}
86
87/// Deserialize YAML string to data structure
88pub fn from_yaml<T: for<'de> Deserialize<'de>>(yaml: &str) -> Result<T> {
89    use tracing::debug;
90
91    debug!(
92        yaml_length = yaml.len(),
93        yaml_preview = %yaml.lines().take(10).collect::<Vec<_>>().join("\\n"),
94        "Deserializing YAML using serde_yaml"
95    );
96
97    let result = serde_yaml::from_str(yaml).context("Failed to deserialize YAML");
98
99    debug!(
100        success = result.is_ok(),
101        error = result
102            .as_ref()
103            .err()
104            .map(|e| e.to_string())
105            .unwrap_or_default(),
106        "YAML deserialization result"
107    );
108
109    result
110}
111
112/// Read and parse YAML file
113pub fn read_yaml_file<T: for<'de> Deserialize<'de>, P: AsRef<Path>>(path: P) -> Result<T> {
114    let content = fs::read_to_string(&path)
115        .with_context(|| format!("Failed to read file: {}", path.as_ref().display()))?;
116
117    from_yaml(&content)
118}
119
120/// Write data structure to YAML file
121pub fn write_yaml_file<T: Serialize, P: AsRef<Path>>(data: &T, path: P) -> Result<()> {
122    let yaml_content = to_yaml(data)?;
123
124    fs::write(&path, yaml_content)
125        .with_context(|| format!("Failed to write file: {}", path.as_ref().display()))?;
126
127    Ok(())
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use serde::{Deserialize, Serialize};
134
135    #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
136    struct TestDiffContent {
137        diff_content: String,
138        description: String,
139    }
140
141    #[test]
142    fn test_multiline_yaml_with_literal_blocks() {
143        let test_data = TestDiffContent {
144            diff_content: "diff --git a/file.txt b/file.txt\nindex 123..456 100644\n--- a/file.txt\n+++ b/file.txt\n@@ -1,3 +1,3 @@\n-old line\n+new line".to_string(),
145            description: "This is a\nmultiline\ndescription".to_string(),
146        };
147
148        let yaml_output = to_yaml(&test_data).unwrap();
149        println!("YAML Output:\n{}", yaml_output);
150
151        // Should use literal block scalar (|) for multiline strings
152        assert!(yaml_output.contains("diff_content: |"));
153        assert!(yaml_output.contains("description: |"));
154
155        // Should contain the actual content without escaped newlines
156        assert!(yaml_output.contains("diff --git"));
157        assert!(yaml_output.contains("--- a/file.txt"));
158        assert!(yaml_output.contains("+++ b/file.txt"));
159
160        // Should not contain escaped newlines in the output
161        assert!(!yaml_output.contains("\\n"));
162
163        // Should round-trip correctly (accounting for trailing newlines added by literal blocks)
164        let deserialized: TestDiffContent = from_yaml(&yaml_output).unwrap();
165
166        // The description should be preserved exactly
167        assert_eq!(test_data.description, deserialized.description);
168
169        // The diff_content may have a trailing newline added by YAML literal block formatting
170        assert!(
171            deserialized.diff_content == test_data.diff_content
172                || deserialized.diff_content == format!("{}\n", test_data.diff_content)
173        );
174    }
175
176    #[test]
177    fn test_yaml_round_trip_preserves_content() {
178        let original = TestDiffContent {
179            diff_content: "line1\nline2\nline3".to_string(),
180            description: "desc line1\ndesc line2".to_string(),
181        };
182
183        let yaml_output = to_yaml(&original).unwrap();
184        let deserialized: TestDiffContent = from_yaml(&yaml_output).unwrap();
185
186        // YAML literal blocks may add trailing newlines, so check content preservation
187        assert_eq!(original.description, deserialized.description);
188        assert!(
189            deserialized.diff_content == original.diff_content
190                || deserialized.diff_content == format!("{}\n", original.diff_content)
191        );
192    }
193
194    #[test]
195    fn test_ai_response_like_yaml_parsing() {
196        // Simulate EXACTLY what the AI is generating based on the debug logs
197        let ai_response_yaml = r#"title: "deps(test): upgrade hedgehog-extras to 0.10.0.0"
198description: |
199  # Changelog
200
201  ```yaml
202  - description: |
203      Upgrade hedgehog-extras dependency from 0.7.1+ to ^>=0.10.0.0 to access newer testing utilities and improvements. Updated type constraints and imports to maintain compatibility with the enhanced testing framework.
204    type:
205      - test           # fixes/modifies tests
206      - maintenance    # not directly related to the code
207  ```
208
209  # Context
210
211  This PR upgrades the `hedgehog-extras` testing library from version 0.7.1+ to 0.10.0.0 to leverage newer testing utilities and improvements. The upgrade requires several compatibility changes to maintain existing test functionality while accessing the enhanced testing framework capabilities.
212
213  The changes ensure that the Cardano CLI test suite continues to work correctly with the updated dependency while taking advantage of improvements in the newer version of hedgehog-extras.
214
215  # How to trust this PR
216
217  **Key areas to review:**
218
219  1. **Dependency constraint update** in `cardano-cli/cardano-cli.cabal` - verify the version constraint change from `>=0.7.1` to `^>=0.10`
220
221  2. **Type signature enhancement** in `Test/Cli/Run/Hash.hs` - the `hash_trip_fun` function now includes additional type constraints (`MonadBaseControl IO m` and `H.MonadAssertion m`) required by hedgehog-extras 0.10
222
223  3. **Import additions** - new imports for `FlexibleContexts` language extension and `MonadBaseControl` to support the updated API
224
225  **Commands to verify the changes:**
226  ```bash
227  # Verify the project builds with new dependencies
228  cabal build cardano-cli-test-lib
229
230  # Run the hash tests specifically
231  cabal test cardano-cli-test --test-options="--pattern Hash"
232
233  # Check that all tests still pass
234  cabal test cardano-cli-test
235  ```
236
237  **Specific changes made:**
238
239  - **cabal.project**: Updated Hackage index-state from 2025-06-22 to 2025-09-10 for latest package availability
240  - **cardano-cli.cabal**: Changed hedgehog-extras constraint from `>=0.7.1` to `^>=0.10`
241  - **Test/Cli/Run/Hash.hs**:
242    - Added `FlexibleContexts` language extension
243    - Imported `MonadBaseControl` from `Control.Monad.Trans.Control`
244    - Extended `hash_trip_fun` type signature with `MonadBaseControl IO m` and `H.MonadAssertion m` constraints
245  - **flake.lock**: Updated dependency hashes to reflect the new package versions
246
247  The type constraint additions are necessary because hedgehog-extras 0.10 has enhanced its monad transformer support, requiring these additional capabilities for proper test execution.
248
249  # Checklist
250
251  - [x] Commit sequence broadly makes sense and commits have useful messages
252  - [x] New tests are added if needed and existing tests are updated. See [Running tests](https://github.com/input-output-hk/cardano-node-wiki/wiki/Running-tests) for more details
253  - [x] Self-reviewed the diff"#;
254
255        // This should parse correctly using our hybrid approach
256        #[derive(serde::Deserialize)]
257        struct PrContent {
258            title: String,
259            description: String,
260        }
261
262        println!("Testing YAML parsing with AI response...");
263        println!("Input length: {} chars", ai_response_yaml.len());
264        println!(
265            "First 200 chars: {}",
266            &ai_response_yaml[..200.min(ai_response_yaml.len())]
267        );
268
269        let pr_content: PrContent = from_yaml(ai_response_yaml).unwrap();
270
271        println!("Parsed title: {}", pr_content.title);
272        println!(
273            "Parsed description length: {}",
274            pr_content.description.len()
275        );
276        println!("Description first 3 lines:");
277        for (i, line) in pr_content.description.lines().take(3).enumerate() {
278            println!("  {}: {}", i + 1, line);
279        }
280
281        assert_eq!(
282            pr_content.title,
283            "deps(test): upgrade hedgehog-extras to 0.10.0.0"
284        );
285        assert!(pr_content.description.contains("# Changelog"));
286        assert!(pr_content.description.contains("# How to trust this PR"));
287        assert!(pr_content.description.contains("**Key areas to review:**"));
288        assert!(pr_content.description.contains("# Checklist"));
289
290        // Verify the multiline content is preserved properly
291        let lines: Vec<&str> = pr_content.description.lines().collect();
292        assert!(
293            lines.len() > 20,
294            "Should have many lines, got {}",
295            lines.len()
296        );
297
298        // Verify the description is much longer than 11 characters
299        assert!(
300            pr_content.description.len() > 100,
301            "Description should be long, got {}",
302            pr_content.description.len()
303        );
304    }
305}