ssv/engine/
fluent_writer.rs

1//! Writes SSV using a fluent interface. Automatically inserts delimiters.
2
3use std::io::Write;
4
5use crate::engine::domain::Domain;
6use crate::engine::LineBreak;
7
8use super::domain::{BytesDomain, DomainStringSlice};
9use super::options::Options;
10use super::{WriteError, WriteResult};
11
12/// Has a fluent interface to write SSV to a byte writer.
13#[doc = generic_item_warning_doc!("FluentWriter")]
14/// Spacing and line-breaks are automatically written as required.
15///
16/// The underlying byte writer is flushed when the [`FluentWriter`] is dropped.
17/// Eventual flush errors will be ignored. Prefer to explicitly call the
18/// [`finish`](FluentWriter::finish) method instead of letting the
19/// [`FluentWriter`] being dropped.
20///
21/// # Example
22///
23/// ```
24/// use ssv::chars::FluentWriter;
25/// let mut output = Vec::new();
26///
27/// let fluent_writer = FluentWriter::new(&mut output);
28///
29/// fluent_writer
30///     .write_value("value")?
31///     .write_value("another value")? // automatic spacing
32///     .write_line_break()?
33///     .write_value("finalvalue")?
34///     .finish()?;
35/// # Ok::<(), ssv::chars::WriteError>(())
36/// ```
37#[derive(Debug)]
38pub struct FluentWriter<D: Domain, W: Write> {
39    inner: W,
40    state: State,
41    options: Options<D>,
42}
43
44impl<D: Domain, W: Write> FluentWriter<D, W> {
45    /// Creates an instance that writes SSV to the given byte writer.
46    pub fn new(inner: W) -> Self {
47        FluentWriter {
48            inner,
49            state: State::LineBegin,
50            options: Options::new(),
51        }
52    }
53
54    /// Writes a value.
55    ///
56    /// The value is enclosed in quotes if required (check the [rules](crate#rules))
57    /// or if the [`always_quoted`](FluentWriter::always_quoted) option is `true`.
58    ///
59    /// If the last wroten item was another value, then the
60    /// [default spacing](FluentWriter::default_spacing) is automatically written
61    /// before this value.
62    ///
63    /// If the last wroten item was a comment, then the
64    /// [default line-break](FluentWriter::default_line_break) is automatically
65    /// written before this value.
66    pub fn write_value(self, value: &D::StringSlice) -> WriteResult<Self> {
67        self.write_value_raw(value, false)
68    }
69
70    /// Writes a value enclosed in quotes.
71    ///
72    /// If the last wroten item was another value, then the
73    /// [default spacing](FluentWriter::default_spacing) is automatically written
74    /// before this value.
75    ///
76    /// If the last wroten item was a comment, then the
77    /// [default line-break](FluentWriter::default_line_break) is automatically
78    /// written before this value.
79    pub fn write_quoted_value(self, value: &D::StringSlice) -> WriteResult<Self> {
80        self.write_value_raw(value, true)
81    }
82
83    fn write_value_raw(mut self, value: &D::StringSlice, quoted: bool) -> WriteResult<Self> {
84        let mut this = match self.state {
85            State::Value => {
86                Self::write_spacing_raw(
87                    &mut self.inner,
88                    self.options.default_spacing().as_bytes(),
89                    &mut self.state,
90                )?;
91                self
92            }
93            State::Comment => self.write_line_break()?,
94            _ => self,
95        };
96
97        let prepared_value = PreparedValue::from(value.as_bytes());
98        let quoted = quoted
99            || prepared_value.must_be_quoted
100            || this.always_quoted()
101            || (this.state == State::LineBegin
102                && prepared_value.bytes.first() == Some(&BytesDomain::HASH));
103
104        if quoted {
105            this.write(&[BytesDomain::QUOTE])?;
106        }
107        this.write(&prepared_value.bytes)?;
108        if quoted {
109            this.write(&[BytesDomain::QUOTE])?;
110        }
111
112        this.state = State::Value;
113        Ok(this)
114    }
115
116    /// Writes the specified spacing.
117    ///
118    /// If the last wroten item was a comment, then the
119    /// [default line-break](FluentWriter::default_line_break) is automatically
120    /// written before this value.
121    pub fn write_spacing(self, spacing: &D::StringSlice) -> WriteResult<Self> {
122        if !D::is_valid_spacing(spacing) {
123            return Err(WriteError::InvalidSpacing);
124        }
125
126        let mut this = match self.state {
127            State::Comment => self.write_line_break()?,
128            _ => self,
129        };
130
131        Self::write_spacing_raw(&mut this.inner, spacing.as_bytes(), &mut this.state)?;
132        Ok(this)
133    }
134
135    fn write_spacing_raw(
136        writer: &mut W,
137        spacing_bytes: &[u8],
138        state: &mut State,
139    ) -> WriteResult<()> {
140        Self::write_raw(writer, spacing_bytes)?;
141        *state = State::Spacing;
142        Ok(())
143    }
144
145    /// Writes the [default line-break](FluentWriter::default_line_break).
146    pub fn write_line_break(self) -> WriteResult<Self> {
147        let line_break = self.default_line_break();
148        self.write_this_line_break(line_break)
149    }
150
151    /// Writes the specified line-break.
152    pub fn write_this_line_break(mut self, line_break: LineBreak) -> WriteResult<Self> {
153        let bytes: &[u8] = match line_break {
154            LineBreak::Lf => &[BytesDomain::LF],
155            LineBreak::CrLf => &[BytesDomain::CR, BytesDomain::LF],
156        };
157        self.write(bytes)?;
158
159        self.state = State::LineBegin;
160        Ok(self)
161    }
162
163    /// Writes the comment.
164    ///
165    /// The HASH sign (`#`) is written before the comment.
166    ///
167    /// If the last wroten item was a value, spacing, or another comment, then
168    /// the [default line-break](FluentWriter::default_line_break) is automatically
169    /// written before the HASH sign and the comment.
170    pub fn write_comment(self, comment: &D::StringSlice) -> WriteResult<Self> {
171        let mut this = match self.state {
172            State::Value | State::Spacing | State::Comment => self.write_line_break()?,
173            _ => self,
174        };
175
176        this.write(&[BytesDomain::HASH])?;
177        this.write(comment.as_bytes())?;
178
179        this.state = State::Comment;
180        Ok(this)
181    }
182
183    fn write(&mut self, bytes: &[u8]) -> WriteResult<()> {
184        Self::write_raw(&mut self.inner, bytes)
185    }
186
187    fn write_raw(writer: &mut W, bytes: &[u8]) -> WriteResult<()> {
188        writer.write_all(bytes)?;
189        Ok(())
190    }
191
192    /// Finalizes the object by flushing the underlying byte writer.
193    ///
194    /// Prefer to explicitly call this method instead of letting the [`FluentWriter`]
195    /// being dropped.
196    pub fn finish(mut self) -> WriteResult<()> {
197        self.inner.flush()?;
198        Ok(())
199    }
200
201    /// Returns the default spacing used by the methods
202    /// [`write_value`](FluentWriter::write_value) and
203    /// [`write_quoted_value`](FluentWriter::write_quoted_value).
204    ///
205    /// This is the same as `self.options().default_spacing()`.
206    pub fn default_spacing(&self) -> &D::StringSlice {
207        self.options.default_spacing()
208    }
209
210    /// Sets the [default spacing](FluentWriter::default_spacing).
211    ///
212    /// This has the same effect as `self.options_mut().set_default_spacing(spacing)`.
213    pub fn set_default_spacing(mut self, spacing: D::String) -> WriteResult<Self> {
214        self.options.set_default_spacing(spacing)?;
215        Ok(self)
216    }
217
218    /// Returns the default line-break used by the methods
219    /// [`write_value`](FluentWriter::write_value),
220    /// [`write_quoted_value`](FluentWriter::write_quoted_value),
221    /// [`write_spacing`](FluentWriter::write_spacing),
222    /// [`write_line_break`](FluentWriter::write_line_break) and
223    /// [`write_comment`](FluentWriter::write_comment).
224    ///
225    /// This is the same as `self.options().default_line_break()`.
226    pub fn default_line_break(&self) -> LineBreak {
227        self.options.default_line_break()
228    }
229
230    /// Sets the [default line-break](FluentWriter::default_line_break).
231    ///
232    /// This has the same effect as `self.options_mut().set_default_line_break(line_break)`.
233    pub fn set_default_line_break(mut self, line_break: LineBreak) -> Self {
234        self.options.set_default_line_break(line_break);
235        self
236    }
237
238    /// Returns whether the [write_value](FluentWriter::write_value) should
239    /// automatically quote the values.
240    ///
241    /// This is the same as `self.options().always_quoted()`.
242    pub fn always_quoted(&self) -> bool {
243        self.options.always_quoted()
244    }
245
246    /// Sets whether the [write_value](FluentWriter::write_value) should
247    /// automatically quote the values.
248    ///
249    /// This is the same as `self.options_mut().set_always_quoted(always_quoted)`.
250    pub fn set_always_quoted(mut self, always_quoted: bool) -> Self {
251        self.options.set_always_quoted(always_quoted);
252        self
253    }
254
255    /// Returns a reference to the associated [`Options`] object.
256    pub fn options(&self) -> &Options<D> {
257        &self.options
258    }
259
260    /// Returns a mutable reference to the associated [`Options`] object.
261    pub fn options_mut(&mut self) -> &mut Options<D> {
262        &mut self.options
263    }
264
265    /// Replaces the associated [`Options`] object.
266    pub fn set_options(mut self, options: Options<D>) -> WriteResult<Self> {
267        self.options = options;
268        Ok(self)
269    }
270}
271
272struct PreparedValue {
273    bytes: Vec<u8>,
274    must_be_quoted: bool,
275}
276
277impl PreparedValue {
278    fn from(original_bytes: &[u8]) -> PreparedValue {
279        let mut only_quotes = true;
280        let mut spacing_or_line_break = false;
281
282        let mut bytes = Vec::new();
283        for byte in original_bytes {
284            bytes.push(*byte);
285
286            if *byte == BytesDomain::QUOTE {
287                bytes.push(*byte);
288            } else {
289                only_quotes = false;
290
291                if *byte == BytesDomain::LF || BytesDomain::is_spacing_element(*byte) {
292                    spacing_or_line_break = true;
293                }
294            }
295        }
296
297        PreparedValue {
298            bytes,
299            must_be_quoted: only_quotes || spacing_or_line_break,
300        }
301    }
302}
303
304#[derive(PartialEq, Eq, Debug)]
305enum State {
306    Value,
307    Spacing,
308    LineBegin,
309    Comment,
310}