Decouple DatabaseJson from files (#11)

Closes #10

Reviewed-on: https://git.wojciechkozlowski.eu/wojtek/musichoard/pulls/11
This commit is contained in:
Wojciech Kozlowski 2023-03-31 16:14:59 +02:00
parent 7400c5ae10
commit 83893f1e7e
4 changed files with 138 additions and 80 deletions

View File

@ -1,46 +1,39 @@
//! Module for storing MusicHoard data in a JSON file database.
use std::fs::File;
use std::io::{Read, Write};
use std::path::Path;
use std::fs::{read_to_string, write};
use std::path::PathBuf;
use serde::de::DeserializeOwned;
use serde::Serialize;
use super::{DatabaseRead, DatabaseWrite};
/// Trait for the JSON database backend.
pub trait DatabaseJsonBackend {
/// Read the JSON string from the backend.
fn read(&self) -> Result<String, std::io::Error>;
/// Write the JSON string to the backend.
fn write(&mut self, json: &str) -> Result<(), std::io::Error>;
}
/// A JSON file database.
pub struct DatabaseJson {
database_file: File,
backend: Box<dyn DatabaseJsonBackend>,
}
impl DatabaseJson {
/// Create a read-only database instance. If the JSON file does not exist, an error is returned.
pub fn reader(path: &Path) -> Result<Self, std::io::Error> {
Ok(Self {
database_file: File::open(path)?,
})
}
/// Create a write-only database instance. If the file does not exist, it is created, if it does
/// exist, it is truncated.
pub fn writer(path: &Path) -> Result<Self, std::io::Error> {
Ok(Self {
database_file: File::create(path)?,
})
pub fn new(backend: Box<dyn DatabaseJsonBackend>) -> Self {
DatabaseJson { backend }
}
}
impl DatabaseRead for DatabaseJson {
fn read<D>(&mut self, collection: &mut D) -> Result<(), std::io::Error>
fn read<D>(&self, collection: &mut D) -> Result<(), std::io::Error>
where
D: DeserializeOwned,
{
// Read entire file to memory as for now this is faster than a buffered read from disk:
// https://github.com/serde-rs/json/issues/160
let mut serialized = String::new();
self.database_file.read_to_string(&mut serialized)?;
let serialized = self.backend.read()?;
*collection = serde_json::from_str(&serialized)?;
Ok(())
}
@ -52,21 +45,54 @@ impl DatabaseWrite for DatabaseJson {
S: Serialize,
{
let serialized = serde_json::to_string(&collection)?;
self.database_file.write_all(serialized.as_bytes())?;
self.backend.write(&serialized)?;
Ok(())
}
}
pub struct DatabaseJsonFile {
path: PathBuf,
}
impl DatabaseJsonFile {
/// Create a database instance that will read/write to the provided path.
pub fn new(path: PathBuf) -> Self {
DatabaseJsonFile { path }
}
}
impl DatabaseJsonBackend for DatabaseJsonFile {
fn read(&self) -> Result<String, std::io::Error> {
// Read entire file to memory as for now this is faster than a buffered read from disk:
// https://github.com/serde-rs/json/issues/160
read_to_string(&self.path)
}
fn write(&mut self, json: &str) -> Result<(), std::io::Error> {
write(&self.path, json)
}
}
#[cfg(test)]
mod tests {
use std::path::Path;
use tempfile::NamedTempFile;
use super::*;
use crate::{Album, AlbumId, Track};
const TEST_FILENAME: &str = "tests/files/database_json_test.json";
struct DatabaseJsonTest {
json: String,
}
impl DatabaseJsonBackend for DatabaseJsonTest {
fn read(&self) -> Result<String, std::io::Error> {
Ok(self.json.clone())
}
fn write(&mut self, json: &str) -> Result<(), std::io::Error> {
assert_eq!(self.json, json);
Ok(())
}
}
fn test_data() -> Vec<Album> {
vec![
@ -109,55 +135,91 @@ mod tests {
]
}
fn album_to_json(album: &Album) -> String {
let album_id_artist = &album.id.artist;
let album_id_year = album.id.year;
let album_id_title = &album.id.title;
let mut album_tracks: Vec<String> = vec![];
for track in album.tracks.iter() {
let track_number = track.number;
let track_title = &track.title;
let mut track_artist: Vec<String> = vec![];
for artist in track.artist.iter() {
track_artist.push(format!("\"{artist}\""))
}
let track_artist = track_artist.join(",");
album_tracks.push(format!(
"{{\
\"number\":{track_number},\
\"title\":\"{track_title}\",\
\"artist\":[{track_artist}]\
}}"
));
}
let album_tracks = album_tracks.join(",");
format!(
"{{\
\"id\":{{\
\"artist\":\"{album_id_artist}\",\
\"year\":{album_id_year},\
\"title\":\"{album_id_title}\"\
}},\"tracks\":[{album_tracks}]\
}}"
)
}
fn albums_to_json(albums: &Vec<Album>) -> String {
let mut albums_strings: Vec<String> = vec![];
for album in albums.iter() {
albums_strings.push(album_to_json(album));
}
let albums_json = albums_strings.join(",");
format!("[{albums_json}]")
}
#[test]
fn write() {
let write_data = test_data();
let temp_file = NamedTempFile::new().unwrap();
DatabaseJson::writer(temp_file.path())
.unwrap()
let backend = DatabaseJsonTest {
json: albums_to_json(&write_data),
};
DatabaseJson::new(Box::new(backend))
.write(&write_data)
.unwrap();
let mut write_data_str = String::new();
File::open(temp_file.path())
.unwrap()
.read_to_string(&mut write_data_str)
.unwrap();
let mut test_data_str = String::new();
File::open(TEST_FILENAME)
.unwrap()
.read_to_string(&mut test_data_str)
.unwrap();
assert_eq!(write_data_str, test_data_str);
}
#[test]
fn read() {
let expected = test_data();
let backend = DatabaseJsonTest {
json: albums_to_json(&expected),
};
let mut read_data: Vec<Album> = vec![];
DatabaseJson::reader(Path::new(TEST_FILENAME))
.unwrap()
DatabaseJson::new(Box::new(backend))
.read(&mut read_data)
.unwrap();
assert_eq!(read_data, test_data());
assert_eq!(read_data, expected);
}
#[test]
fn reverse() {
let expected = test_data();
let backend = DatabaseJsonTest {
json: albums_to_json(&expected),
};
let mut database = DatabaseJson::new(Box::new(backend));
let write_data = test_data();
let mut read_data: Vec<Album> = vec![];
let temp_file = NamedTempFile::new().unwrap();
DatabaseJson::writer(temp_file.path())
.unwrap()
.write(&write_data)
.unwrap();
DatabaseJson::reader(temp_file.path())
.unwrap()
.read(&mut read_data)
.unwrap();
database.write(&write_data).unwrap();
database.read(&mut read_data).unwrap();
assert_eq!(write_data, read_data);
}

View File

@ -8,7 +8,7 @@ pub mod json;
/// Trait for database reads.
pub trait DatabaseRead {
/// Read collection from the database.
fn read<D>(&mut self, collection: &mut D) -> Result<(), std::io::Error>
fn read<D>(&self, collection: &mut D) -> Result<(), std::io::Error>
where
D: DeserializeOwned;
}

View File

@ -80,7 +80,7 @@ impl QueryArgsBeets for Query {
/// Trait for invoking beets commands.
pub trait BeetsExecutor {
/// Invoke beets with the provided arguments.
fn exec(&mut self, arguments: Vec<String>) -> Result<Vec<String>, Error>;
fn exec(&mut self, arguments: &[String]) -> Result<Vec<String>, Error>;
}
/// Struct for interacting with the music library via beets.
@ -106,7 +106,7 @@ impl Beets {
impl Library for Beets {
fn list(&mut self, query: &Query) -> Result<Vec<Album>, Error> {
let cmd = Self::list_cmd_and_args(query);
let output = self.executor.exec(cmd)?;
let output = self.executor.exec(&cmd)?;
Self::list_to_albums(output)
}
}
@ -209,7 +209,7 @@ impl Default for SystemExecutor {
}
impl BeetsExecutor for SystemExecutor {
fn exec(&mut self, arguments: Vec<String>) -> Result<Vec<String>, Error> {
fn exec(&mut self, arguments: &[String]) -> Result<Vec<String>, Error> {
let output = Command::new(&self.bin).args(arguments).output()?;
let output = std::str::from_utf8(&output.stdout)?;
Ok(output.split('\n').map(|s| s.to_string()).collect())
@ -226,10 +226,8 @@ mod tests {
}
impl BeetsExecutor for TestExecutor {
fn exec(&mut self, arguments: Vec<String>) -> Result<Vec<String>, Error> {
if self.arguments.is_some() {
assert_eq!(self.arguments.take().unwrap(), arguments);
}
fn exec(&mut self, arguments: &[String]) -> Result<Vec<String>, Error> {
assert_eq!(&self.arguments.take().unwrap(), arguments);
self.output.take().unwrap()
}
}
@ -303,6 +301,14 @@ mod tests {
strings
}
fn albums_to_beets_string(albums: &Vec<Album>) -> Vec<String> {
let mut strings = vec![];
for album in albums.iter() {
strings.append(&mut album_to_beets_string(album));
}
strings
}
#[test]
fn test_query() {
let query = Query::new()
@ -341,10 +347,7 @@ mod tests {
#[test]
fn test_list_ordered() {
let expected = test_data();
let mut output = vec![];
for album in expected.iter() {
output.append(&mut album_to_beets_string(album));
}
let output = albums_to_beets_string(&expected);
let executor = TestExecutor {
arguments: Some(vec!["ls".to_string(), Beets::LIST_FORMAT_ARG.to_string()]),
@ -359,10 +362,7 @@ mod tests {
#[test]
fn test_list_unordered() {
let mut expected = test_data();
let mut output = vec![];
for album in expected.iter() {
output.append(&mut album_to_beets_string(album));
}
let mut output = albums_to_beets_string(&expected);
let last = output.len() - 1;
output.swap(0, last);
@ -391,10 +391,7 @@ mod tests {
expected[1].id.year = expected[0].id.year;
expected[1].id.title = expected[0].id.title.clone();
let mut output = vec![];
for album in expected.iter() {
output.append(&mut album_to_beets_string(album));
}
let output = albums_to_beets_string(&expected);
let executor = TestExecutor {
arguments: Some(vec!["ls".to_string(), Beets::LIST_FORMAT_ARG.to_string()]),

View File

@ -1 +0,0 @@
[{"id":{"artist":"Artist A","year":1998,"title":"Release group A"},"tracks":[{"number":1,"title":"Track A.1","artist":["Artist A.A"]},{"number":2,"title":"Track A.2","artist":["Artist A.A"]},{"number":3,"title":"Track A.3","artist":["Artist A.A","Artist A.B"]}]},{"id":{"artist":"Artist B","year":2008,"title":"Release group B"},"tracks":[{"number":1,"title":"Track B.1","artist":["Artist B.A"]}]}]