| // Copyright 2022 The Pigweed Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| // use this file except in compliance with the License. You may obtain a copy of |
| // the License at |
| // |
| // https://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| // License for the specific language governing permissions and limitations under |
| // the License. |
| |
| use std::{ |
| io::Write, |
| path::{Path, PathBuf}, |
| }; |
| |
| use serde::{de::DeserializeOwned, Serialize}; |
| |
| use crate::{Error, Result}; |
| |
| /// A file storing data in some serialized format. |
| #[derive(Debug)] |
| pub struct File { |
| path: PathBuf, |
| } |
| |
| impl File { |
| pub(super) fn new(path: impl AsRef<Path>) -> Self { |
| Self { |
| path: path.as_ref().to_path_buf(), |
| } |
| } |
| |
| /// Returns the full path to the file. |
| #[must_use] |
| pub fn path(&self) -> &Path { |
| &self.path |
| } |
| |
| /// Reads the contents of the file into a string. |
| /// |
| /// # Errors |
| /// Returns an error if the file could not be read. |
| pub fn read_text(&self) -> Result<String> { |
| std::fs::read_to_string(&self.path).map_err(Error::from) |
| } |
| |
| /// Writes a string to the file. |
| /// |
| /// Creates the file if it does not exist, or overwrites existing contents. |
| /// |
| /// # Errors |
| /// Returns an error if the file could not be written. |
| pub fn write_text(&self, contents: &str) -> Result<()> { |
| std::fs::write(&self.path, contents).map_err(Error::from) |
| } |
| |
| /// Writes a serializable struct to the file as TOML. |
| /// |
| /// Creates the file if it does not exist, or overwrites existing contents. |
| /// |
| /// # Errors |
| /// Returns an error if the file could not be written, or the data is not |
| /// serializable. |
| pub fn serialize_toml<T: Serialize>(&self, value: &T) -> Result<()> { |
| let mut f = std::fs::File::create(&self.path).map_err(Error::file(&self.path))?; |
| let string = toml::to_string(value)?; |
| f.write_all(string.as_bytes()) |
| .map_err(Error::file(&self.path))?; |
| Ok(()) |
| } |
| |
| /// Reads TOML data from the file into a struct. |
| /// |
| /// # Errors |
| /// Returns an error if the file could not be read, or the data is not |
| /// valid TOML. |
| pub fn deserialize_toml<T: DeserializeOwned>(&self) -> Result<T> { |
| let string = std::fs::read_to_string(&self.path).map_err(Error::file(&self.path))?; |
| let value = toml::from_str(&string)?; |
| Ok(value) |
| } |
| |
| /// Modifies TOML data stored in the file, writing the updated values back |
| /// to disk. |
| /// |
| /// # Errors |
| /// Returns an error if the file could not be accessed, or data could not be |
| /// serialized. |
| pub fn modify_toml<T: Serialize + DeserializeOwned>( |
| &self, |
| func: impl FnOnce(&mut T) -> Result<()>, |
| ) -> Result<()> { |
| let mut value = self.deserialize_toml()?; |
| func(&mut value)?; |
| self.serialize_toml(&value) |
| } |
| |
| pub(crate) fn relative_file(&self, path: &Path) -> Self { |
| Self::new( |
| self.path |
| .parent() |
| .expect("file must have parent") |
| .join(path), |
| ) |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use serde::Deserialize; |
| |
| use super::*; |
| |
| #[derive(Debug, Deserialize, PartialEq, Serialize)] |
| struct Foo { |
| value: String, |
| number: u8, |
| bar: Option<Bar>, |
| } |
| |
| #[derive(Debug, Deserialize, PartialEq, Serialize)] |
| struct Bar { |
| path: PathBuf, |
| } |
| |
| const SERIALIZED_DATA: &str = r#"value = "encodeme" |
| number = 199 |
| |
| [bar] |
| path = "/usr/bin" |
| "#; |
| |
| #[test] |
| fn serialize_success() { |
| let tmpfile = tempfile::NamedTempFile::new().unwrap(); |
| let file = File::new(tmpfile.path()); |
| |
| let foo = Foo { |
| value: "encodeme".into(), |
| number: 199, |
| bar: Some(Bar { |
| path: PathBuf::from("/usr/bin"), |
| }), |
| }; |
| |
| file.serialize_toml(&foo).unwrap(); |
| let string = std::fs::read_to_string(file.path()).unwrap(); |
| assert_eq!(string, SERIALIZED_DATA); |
| } |
| |
| #[test] |
| fn deserialize_success() { |
| let tmpfile = tempfile::NamedTempFile::new().unwrap(); |
| let file = File::new(tmpfile.path()); |
| |
| std::fs::write(file.path(), SERIALIZED_DATA).unwrap(); |
| let foo = file.deserialize_toml::<Foo>().unwrap(); |
| assert_eq!(foo.value, "encodeme".to_string()); |
| assert_eq!(foo.number, 199); |
| assert_eq!( |
| foo.bar, |
| Some(Bar { |
| path: PathBuf::from("/usr/bin") |
| }) |
| ); |
| } |
| |
| #[test] |
| fn deserialize_nonexisting_file() { |
| let file = File::new("./non-existing-file.xyz"); |
| let e = file.deserialize_toml::<Foo>().unwrap_err(); |
| assert!(matches!(e, Error::File(_, _))); |
| } |
| |
| #[test] |
| fn deserialize_invalid_data() { |
| let tmpfile = tempfile::NamedTempFile::new().unwrap(); |
| let file = File::new(tmpfile.path()); |
| |
| std::fs::write(file.path(), "invalid data").unwrap(); |
| let e = file.deserialize_toml::<Foo>().unwrap_err(); |
| assert!(matches!(e, Error::Serialization(_))); |
| } |
| |
| #[test] |
| fn modify_success() { |
| let tmpfile = tempfile::NamedTempFile::new().unwrap(); |
| let file = File::new(tmpfile.path()); |
| |
| let foo = Foo { |
| value: "encodeme".into(), |
| number: 199, |
| bar: Some(Bar { |
| path: PathBuf::from("/usr/bin"), |
| }), |
| }; |
| |
| file.serialize_toml(&foo).unwrap(); |
| file.modify_toml::<Foo>(|value| { |
| value.bar = None; |
| value.number += 2; |
| Ok(()) |
| }) |
| .unwrap(); |
| |
| let result = file.deserialize_toml::<Foo>().unwrap(); |
| assert_eq!(result.value, foo.value); |
| assert_eq!(result.number, 201); |
| assert_eq!(result.bar, None); |
| } |
| } |