blob: db34bd16fcd8dd689f816a3fcd3b33be352de5b3 [file] [log] [blame]
// 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.
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
}
/// 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)
}
}
#[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);
}
}