Skip to main content

omni_dev/data/
yaml.rs

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