punktf_lib/profile/
transform.rs

1//! Transforms run once for each defined dotfile during the deploy process.
2//!
3//! They can either be specified for a whole profile, in which case each dotfile
4//! is transformed by them or they can be attached to a specific dotfile.
5//!
6//! The transformation takes place after the template resolving and takes the
7//! contents in a textual representation. After processing the text a new text
8//! must be returned.
9
10use std::fmt;
11
12use color_eyre::Result;
13
14/// A transform takes the contents of a dotfile, processes it and returns a new
15/// version of the content.
16///
17/// The dotfile is either the text of a resolved template or a non-template
18/// dotfile.
19pub trait Transform {
20	/// Takes a string as input, processes it and returns a new version of it.
21	///
22	/// # Errors
23	///
24	/// If any error occurs during the processing it can be returned.
25	fn transform(&self, content: String) -> Result<String>;
26}
27
28/// List of all available [`Transform`s](`crate::profile::transform::Transform`).
29///
30/// These can be added to a [`Profile`](`crate::profile::Profile`) or a
31/// [`Dotfile`](`crate::profile::dotfile::Dotfile`) to modify the text content.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
33#[serde(deny_unknown_fields)]
34pub enum ContentTransformer {
35	/// Transformer which replaces line termination characters with either unix
36	/// style (`\n`) or windows style (`\r\b`).
37	LineTerminator(LineTerminator),
38}
39
40impl Transform for ContentTransformer {
41	fn transform(&self, content: String) -> Result<String> {
42		match self {
43			Self::LineTerminator(lt) => lt.transform(content),
44		}
45	}
46}
47
48impl fmt::Display for ContentTransformer {
49	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
50		fmt::Display::fmt(&self, f)
51	}
52}
53
54/// Transformer which replaces line termination characters with either unix
55/// style (`\n`) or windows style (`\r\b`).
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
57pub enum LineTerminator {
58	/// Replaces all occurrences of `\r\n` with `\n` (unix style).
59	LF,
60
61	/// Replaces all occurrences of `\n` with `\r\n` (windows style).
62	CRLF,
63}
64
65impl Transform for LineTerminator {
66	fn transform(&self, mut content: String) -> Result<String> {
67		match self {
68			Self::LF => Ok(content.replace("\r\n", "\n")),
69			Self::CRLF => {
70				let lf_idxs = content.match_indices('\n');
71				let mut cr_idxs = content.match_indices('\r').peekable();
72
73				// Allowed as it not needless here, the index iterator have a immutable ref
74				// and are still alive when the string gets modified. To "unborrow" the
75				// collect is necessary.
76				#[allow(clippy::needless_collect)]
77				let lf_idxs = lf_idxs
78					.filter_map(|(lf_idx, _)| {
79						while matches!(cr_idxs.peek(), Some((cr_idx,_)) if cr_idx + 1 < lf_idx) {
80							// pop standalone `\r`
81							let _ = cr_idxs.next().expect("Failed to advance peeked iterator");
82						}
83
84						if matches!(cr_idxs.peek(), Some((cr_idx, _)) if cr_idx + 1 == lf_idx) {
85							// pop matched cr_idx
86							let _ = cr_idxs.next().expect("Failed to advance peeked iterator");
87							None
88						} else {
89							Some(lf_idx)
90						}
91					})
92					.collect::<Vec<_>>();
93
94				for (offset, lf_idx) in lf_idxs.into_iter().enumerate() {
95					content.insert(lf_idx + offset, '\r');
96				}
97
98				Ok(content)
99			}
100		}
101	}
102}
103
104impl fmt::Display for LineTerminator {
105	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
106		fmt::Debug::fmt(&self, f)
107	}
108}
109
110#[cfg(test)]
111mod tests {
112	use pretty_assertions::assert_eq;
113
114	use super::*;
115
116	#[test]
117	fn line_terminator_lf() -> Result<()> {
118		const CONTENT: &str = "Hello\r\nWorld\nHow\nare\r\nyou today?\r\r\r\nLast line\r\\n";
119
120		assert_eq!(
121			LineTerminator::LF.transform(String::from(CONTENT))?,
122			"Hello\nWorld\nHow\nare\nyou today?\r\r\nLast line\r\\n"
123		);
124
125		Ok(())
126	}
127
128	#[test]
129	fn line_terminator_crlf() -> Result<()> {
130		const CONTENT: &str = "Hello\r\nWorld\nHow\nare\r\nyou today?\r\r\r\nLast line\r\\n";
131
132		assert_eq!(
133			LineTerminator::CRLF.transform(String::from(CONTENT))?,
134			"Hello\r\nWorld\r\nHow\r\nare\r\nyou today?\r\r\r\nLast line\r\\n"
135		);
136
137		Ok(())
138	}
139}