1use crate::{Block, Entry, Library, Result, Value};
4use std::io::{self, Write};
5
6#[derive(Debug, Clone)]
8pub struct WriterConfig {
9 pub indent: String,
11 pub align_values: bool,
13 pub max_line_length: usize,
15 pub sort_entries: bool,
17 pub sort_fields: bool,
19}
20
21impl Default for WriterConfig {
22 fn default() -> Self {
23 Self {
24 indent: " ".to_string(),
25 align_values: false,
26 max_line_length: 80,
27 sort_entries: false,
28 sort_fields: false,
29 }
30 }
31}
32
33#[derive(Debug)]
35pub struct Writer<W: Write> {
36 writer: W,
37 config: WriterConfig,
38}
39
40impl<W: Write> Writer<W> {
41 pub fn new(writer: W) -> Self {
43 Self {
44 writer,
45 config: WriterConfig::default(),
46 }
47 }
48
49 pub const fn with_config(writer: W, config: WriterConfig) -> Self {
51 Self { writer, config }
52 }
53
54 #[must_use]
56 pub fn config_mut(&mut self) -> &mut WriterConfig {
57 &mut self.config
58 }
59
60 #[must_use]
62 pub fn into_inner(self) -> W {
63 self.writer
64 }
65
66 pub fn write_library(&mut self, library: &Library) -> io::Result<()> {
68 if self.config.sort_entries {
69 return self.write_library_sorted(library);
70 }
71
72 for (index, block) in library.blocks().into_iter().enumerate() {
73 if index > 0 {
74 writeln!(self.writer)?;
75 }
76 match block {
77 Block::Entry(entry, _) => self.write_entry(entry)?,
78 Block::String(definition) => {
79 self.write_string(&definition.name, &definition.value)?;
80 }
81 Block::Preamble(preamble) => self.write_preamble(&preamble.value)?,
82 Block::Comment(comment) => self.write_comment(comment.text())?,
83 Block::Failed(failed) => self.writer.write_all(failed.raw.as_bytes())?,
84 }
85 }
86
87 Ok(())
88 }
89
90 fn write_library_sorted(&mut self, library: &Library) -> io::Result<()> {
91 for preamble in library.preambles() {
93 self.write_preamble(&preamble.value)?;
94 writeln!(self.writer)?;
95 }
96
97 let mut strings: Vec<_> = library.strings().iter().collect();
99 if self.config.sort_entries {
100 strings.sort_by(|a, b| a.name.cmp(&b.name));
101 }
102
103 for definition in strings {
104 self.write_string(&definition.name, &definition.value)?;
105 writeln!(self.writer)?;
106 }
107
108 let mut entries = library.entries().iter().collect::<Vec<_>>();
110 if self.config.sort_entries {
111 entries.sort_by(|a, b| a.key.cmp(&b.key));
112 }
113
114 for (i, entry) in entries.iter().enumerate() {
115 if i > 0 {
116 writeln!(self.writer)?;
117 }
118 self.write_entry(entry)?;
119 }
120
121 Ok(())
122 }
123
124 pub fn write_entry(&mut self, entry: &Entry) -> io::Result<()> {
126 writeln!(self.writer, "@{}{{{},", entry.ty, entry.key)?;
127
128 let mut fields = entry.fields().to_vec();
129 if self.config.sort_fields {
130 fields.sort_by(|a, b| a.name.cmp(&b.name));
131 }
132
133 let max_name_len = if self.config.align_values {
135 fields.iter().map(|f| f.name.len()).max().unwrap_or(0)
136 } else {
137 0
138 };
139
140 for (i, field) in fields.iter().enumerate() {
141 write!(self.writer, "{}", self.config.indent)?;
142 write!(self.writer, "{}", field.name)?;
143
144 if self.config.align_values {
145 let padding = max_name_len - field.name.len();
146 write!(self.writer, "{}", " ".repeat(padding))?;
147 }
148
149 write!(self.writer, " = ")?;
150 self.write_value(&field.value)?;
151
152 if i < fields.len() - 1 {
153 writeln!(self.writer, ",")?;
154 } else {
155 writeln!(self.writer)?;
156 }
157 }
158
159 writeln!(self.writer, "}}")?;
160 Ok(())
161 }
162
163 fn write_string(&mut self, name: &str, value: &Value) -> io::Result<()> {
165 write!(self.writer, "@string{{{name} = ")?;
166 self.write_value(value)?;
167 writeln!(self.writer, "}}")?;
168 Ok(())
169 }
170
171 fn write_preamble(&mut self, value: &Value) -> io::Result<()> {
173 write!(self.writer, "@preamble{{")?;
174 self.write_value(value)?;
175 writeln!(self.writer, "}}")?;
176 Ok(())
177 }
178
179 fn write_comment(&mut self, text: &str) -> io::Result<()> {
181 let trimmed = text.trim_start();
182 if trimmed.starts_with('%') || trimmed.starts_with('@') {
183 self.writer.write_all(text.as_bytes())?;
184 if !text.ends_with('\n') {
185 writeln!(self.writer)?;
186 }
187 } else {
188 writeln!(self.writer, "@comment{{{text}}}")?;
189 }
190 Ok(())
191 }
192
193 fn write_value(&mut self, value: &Value) -> io::Result<()> {
195 match value {
196 Value::Literal(s) => {
197 if needs_quoting(s) {
199 write!(self.writer, "\"{}\"", escape_quotes(s))?;
200 } else {
201 write!(self.writer, "{{{s}}}")?;
202 }
203 }
204 Value::Number(n) => write!(self.writer, "{n}")?,
205 Value::Variable(name) => write!(self.writer, "{name}")?,
206 Value::Concat(parts) => {
207 for (i, part) in parts.iter().enumerate() {
208 if i > 0 {
209 write!(self.writer, " # ")?;
210 }
211 self.write_value(part)?;
212 }
213 }
214 }
215 Ok(())
216 }
217}
218
219#[must_use]
221fn needs_quoting(s: &str) -> bool {
222 s.contains(['{', '}', ',', '='])
223}
224
225#[must_use]
227fn escape_quotes(s: &str) -> String {
228 s.replace('"', "\\\"")
229}
230
231#[must_use = "Check the result to detect serialization errors"]
233pub fn to_string(library: &Library) -> Result<String> {
234 let mut buf = Vec::new();
235 let mut writer = Writer::new(&mut buf);
236 writer.write_library(library)?;
237 Ok(String::from_utf8(buf).expect("valid UTF-8"))
238}
239
240#[must_use = "Check the result to detect IO or serialization errors"]
242pub fn to_file(library: &Library, path: impl AsRef<std::path::Path>) -> Result<()> {
243 let file = std::fs::File::create(path)?;
244 let mut writer = Writer::new(file);
245 writer.write_library(library)?;
246 Ok(())
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252 use crate::model::{EntryType, Field};
253 use std::borrow::Cow;
254
255 #[test]
256 fn test_write_entry() {
257 let entry = Entry {
258 ty: EntryType::Article,
259 key: Cow::Borrowed("test2023"),
260 fields: vec![
261 Field::new("author", Value::Literal(Cow::Borrowed("John Doe"))),
262 Field::new("title", Value::Literal(Cow::Borrowed("Test Article"))),
263 Field::new("year", Value::Number(2023)),
264 ],
265 };
266
267 let mut buf = Vec::new();
268 let mut writer = Writer::new(&mut buf);
269 writer.write_entry(&entry).unwrap();
270
271 let result = String::from_utf8(buf).unwrap();
272 assert!(result.contains("@article{test2023,"));
273 assert!(result.contains("author = {John Doe}"));
274 assert!(result.contains("title = {Test Article}"));
275 assert!(result.contains("year = 2023"));
276 }
277}