1mod document;
8
9pub use document::{extract_notes, update_document};
10
11use bake::{Context, Error, Result};
12use std::fs;
13use std::fs::{File, Permissions};
14use std::io::{self, Write};
15use std::path::{Path, PathBuf};
16
17fn persist_file(temporary: tempfile::NamedTempFile, path: &Path) -> Result<()> {
18 temporary
19 .persist(path)
20 .map(|_| ())
21 .map_err(|error| Error::from(error.error))
22}
23
24trait UpdateIo {
25 fn create_temporary(&mut self, directory: &Path) -> io::Result<tempfile::NamedTempFile>;
26 fn write(&mut self, file: &mut File, contents: &[u8]) -> io::Result<()>;
27 fn permissions(&mut self, path: &Path) -> io::Result<Permissions>;
28 fn set_permissions(&mut self, file: &File, permissions: Permissions) -> io::Result<()>;
29 fn sync_all(&mut self, file: &File) -> io::Result<()>;
30 fn persist(&mut self, temporary: tempfile::NamedTempFile, path: &Path) -> Result<()>;
31}
32
33struct SystemUpdateIo;
34
35impl UpdateIo for SystemUpdateIo {
36 fn create_temporary(&mut self, directory: &Path) -> io::Result<tempfile::NamedTempFile> {
37 tempfile::NamedTempFile::new_in(directory)
38 }
39
40 fn write(&mut self, file: &mut File, contents: &[u8]) -> io::Result<()> {
41 file.write_all(contents)
42 }
43
44 fn permissions(&mut self, path: &Path) -> io::Result<Permissions> {
45 fs::metadata(path).map(|metadata| metadata.permissions())
46 }
47
48 fn set_permissions(&mut self, file: &File, permissions: Permissions) -> io::Result<()> {
49 file.set_permissions(permissions)
50 }
51
52 fn sync_all(&mut self, file: &File) -> io::Result<()> {
53 file.sync_all()
54 }
55
56 fn persist(&mut self, temporary: tempfile::NamedTempFile, path: &Path) -> Result<()> {
57 persist_file(temporary, path)
58 }
59}
60
61fn update_file_with(
62 directory: &Path,
63 path: &Path,
64 updated: &str,
65 operations: &mut impl UpdateIo,
66) -> Result<()> {
67 let mut temporary = operations.create_temporary(directory)?;
68 operations.write(temporary.as_file_mut(), updated.as_bytes())?;
69 let permissions = operations.permissions(path)?;
70 operations.set_permissions(temporary.as_file(), permissions)?;
71 operations.sync_all(temporary.as_file())?;
72 operations.persist(temporary, path)?;
73 Ok(())
74}
75
76#[bake::task]
78pub fn notes(
79 context: &mut Context,
80 version: String,
81 #[bake(
82 default = "releases.md",
83 help = "Release document relative to the project root."
84 )]
85 path: PathBuf,
86) -> Result<String> {
87 let path = context.root().join(path);
88 let document = fs::read_to_string(&path)
89 .map_err(|error| Error::new(format!("{}: {error}", path.display())))?;
90 Ok(extract_notes(&document, &version)?.to_owned())
91}
92
93#[bake::task]
95pub fn update(
96 context: &mut Context,
97 version: String,
98 #[bake(default = "releases.md")] path: PathBuf,
99) -> Result<()> {
100 let path = context.root().join(path).canonicalize()?;
101 let document = fs::read_to_string(&path)?;
102 let updated = update_document(&document, &version)?;
103 let directory = path.parent().unwrap_or(path.as_path());
104 update_file_with(directory, &path, &updated, &mut SystemUpdateIo)
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 #[test]
112 fn persist_file_reports_a_missing_destination_directory() {
113 let directory = tempfile::tempdir().unwrap();
114 let temporary = tempfile::NamedTempFile::new_in(directory.path()).unwrap();
115 let destination = directory.path().join("missing/releases.md");
116
117 assert!(persist_file(temporary, &destination).is_err());
118 }
119
120 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
121 enum UpdateOperation {
122 CreateTemporary,
123 Write,
124 Permissions,
125 SetPermissions,
126 Sync,
127 Persist,
128 }
129
130 struct FailingUpdateIo {
131 fail_at: Option<UpdateOperation>,
132 system: SystemUpdateIo,
133 }
134
135 impl FailingUpdateIo {
136 fn new(fail_at: Option<UpdateOperation>) -> Self {
137 Self {
138 fail_at,
139 system: SystemUpdateIo,
140 }
141 }
142
143 fn fail_if(&self, operation: UpdateOperation) -> io::Result<()> {
144 if self.fail_at == Some(operation) {
145 Err(io::Error::other("simulated update failure"))
146 } else {
147 Ok(())
148 }
149 }
150 }
151
152 impl UpdateIo for FailingUpdateIo {
153 fn create_temporary(&mut self, directory: &Path) -> io::Result<tempfile::NamedTempFile> {
154 self.fail_if(UpdateOperation::CreateTemporary)?;
155 self.system.create_temporary(directory)
156 }
157
158 fn write(&mut self, file: &mut File, contents: &[u8]) -> io::Result<()> {
159 self.fail_if(UpdateOperation::Write)?;
160 self.system.write(file, contents)
161 }
162
163 fn permissions(&mut self, path: &Path) -> io::Result<Permissions> {
164 self.fail_if(UpdateOperation::Permissions)?;
165 self.system.permissions(path)
166 }
167
168 fn set_permissions(&mut self, file: &File, permissions: Permissions) -> io::Result<()> {
169 self.fail_if(UpdateOperation::SetPermissions)?;
170 self.system.set_permissions(file, permissions)
171 }
172
173 fn sync_all(&mut self, file: &File) -> io::Result<()> {
174 self.fail_if(UpdateOperation::Sync)?;
175 self.system.sync_all(file)
176 }
177
178 fn persist(&mut self, temporary: tempfile::NamedTempFile, path: &Path) -> Result<()> {
179 self.fail_if(UpdateOperation::Persist)?;
180 self.system.persist(temporary, path)
181 }
182 }
183
184 #[test]
185 fn update_file_failures_preserve_the_original_document() {
186 for operation in [
187 UpdateOperation::CreateTemporary,
188 UpdateOperation::Write,
189 UpdateOperation::Permissions,
190 UpdateOperation::SetPermissions,
191 UpdateOperation::Sync,
192 UpdateOperation::Persist,
193 ] {
194 let directory = tempfile::tempdir().unwrap();
195 let destination = directory.path().join("releases.md");
196 fs::write(&destination, "## Unreleased\n").unwrap();
197 let mut operations = FailingUpdateIo::new(Some(operation));
198
199 assert!(
200 update_file_with(directory.path(), &destination, "## v1\n", &mut operations)
201 .is_err()
202 );
203 assert_eq!(fs::read_to_string(destination).unwrap(), "## Unreleased\n");
204 }
205 }
206
207 #[test]
208 fn update_file_succeeds_when_all_operations_succeed() {
209 let directory = tempfile::tempdir().unwrap();
210 let destination = directory.path().join("releases.md");
211 fs::write(&destination, "## Unreleased\n").unwrap();
212 let mut operations = FailingUpdateIo::new(None);
213
214 update_file_with(directory.path(), &destination, "## v1\n", &mut operations).unwrap();
215
216 assert_eq!(fs::read_to_string(destination).unwrap(), "## v1\n");
217 }
218}