omni_dev/data/
amendments.rs1use anyhow::{Context, Result};
4use serde::{Deserialize, Serialize};
5use std::fs;
6use std::path::Path;
7
8#[derive(Debug, Serialize, Deserialize)]
10pub struct AmendmentFile {
11 pub amendments: Vec<Amendment>,
13}
14
15#[derive(Debug, Serialize, Deserialize)]
17pub struct Amendment {
18 pub commit: String,
20 pub message: String,
22}
23
24impl AmendmentFile {
25 pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
27 let content = fs::read_to_string(&path).with_context(|| {
28 format!("Failed to read amendment file: {}", path.as_ref().display())
29 })?;
30
31 let amendment_file: AmendmentFile =
32 serde_yaml::from_str(&content).context("Failed to parse YAML amendment file")?;
33
34 amendment_file.validate()?;
35
36 Ok(amendment_file)
37 }
38
39 pub fn validate(&self) -> Result<()> {
41 for (i, amendment) in self.amendments.iter().enumerate() {
43 amendment
44 .validate()
45 .with_context(|| format!("Invalid amendment at index {}", i))?;
46 }
47
48 Ok(())
49 }
50
51 pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
53 let yaml_content =
54 serde_yaml::to_string(self).context("Failed to serialize amendments to YAML")?;
55
56 let formatted_yaml = self.format_multiline_yaml(&yaml_content);
58
59 fs::write(&path, formatted_yaml).with_context(|| {
60 format!(
61 "Failed to write amendment file: {}",
62 path.as_ref().display()
63 )
64 })?;
65
66 Ok(())
67 }
68
69 fn format_multiline_yaml(&self, yaml: &str) -> String {
71 let mut result = String::new();
72 let lines: Vec<&str> = yaml.lines().collect();
73 let mut i = 0;
74
75 while i < lines.len() {
76 let line = lines[i];
77
78 if line.trim_start().starts_with("message:") && line.contains('"') {
80 let indent = line.len() - line.trim_start().len();
81 let indent_str = " ".repeat(indent);
82
83 if let Some(start_quote) = line.find('"') {
85 if let Some(end_quote) = line.rfind('"') {
86 if start_quote != end_quote {
87 let quoted_content = &line[start_quote + 1..end_quote];
88
89 if quoted_content.contains("\\n") {
91 result.push_str(&format!("{}message: |\n", indent_str));
93
94 let unescaped = quoted_content.replace("\\n", "\n");
96 for (line_idx, content_line) in unescaped.lines().enumerate() {
97 if line_idx == 0 && content_line.trim().is_empty() {
98 continue;
100 }
101 result.push_str(&format!("{} {}\n", indent_str, content_line));
102 }
103 i += 1;
104 continue;
105 }
106 }
107 }
108 }
109 }
110
111 result.push_str(line);
113 result.push('\n');
114 i += 1;
115 }
116
117 result
118 }
119}
120
121impl Amendment {
122 pub fn new(commit: String, message: String) -> Self {
124 Self { commit, message }
125 }
126
127 pub fn validate(&self) -> Result<()> {
129 if self.commit.len() != 40 {
131 anyhow::bail!(
132 "Commit hash must be exactly 40 characters long, got: {}",
133 self.commit.len()
134 );
135 }
136
137 if !self.commit.chars().all(|c| c.is_ascii_hexdigit()) {
138 anyhow::bail!("Commit hash must contain only hexadecimal characters");
139 }
140
141 if !self
142 .commit
143 .chars()
144 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
145 {
146 anyhow::bail!("Commit hash must be lowercase");
147 }
148
149 if self.message.trim().is_empty() {
151 anyhow::bail!("Commit message cannot be empty");
152 }
153
154 Ok(())
155 }
156}