From 83893f1e7e64c53c903edd70d7fc5a9913fdf749 Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Fri, 31 Mar 2023 16:14:59 +0200 Subject: [PATCH] Decouple DatabaseJson from files (#11) Closes #10 Reviewed-on: https://git.wojciechkozlowski.eu/wojtek/musichoard/pulls/11 --- src/database/json.rs | 180 +++++++++++++++++++--------- src/database/mod.rs | 2 +- src/library/beets.rs | 35 +++--- tests/files/database_json_test.json | 1 - 4 files changed, 138 insertions(+), 80 deletions(-) delete mode 100644 tests/files/database_json_test.json diff --git a/src/database/json.rs b/src/database/json.rs index c87d533..05477cb 100644 --- a/src/database/json.rs +++ b/src/database/json.rs @@ -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; + + /// 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, } 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 { - 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 { - Ok(Self { - database_file: File::create(path)?, - }) + pub fn new(backend: Box) -> Self { + DatabaseJson { backend } } } impl DatabaseRead for DatabaseJson { - fn read(&mut self, collection: &mut D) -> Result<(), std::io::Error> + fn read(&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 { + // 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 { + Ok(self.json.clone()) + } + + fn write(&mut self, json: &str) -> Result<(), std::io::Error> { + assert_eq!(self.json, json); + Ok(()) + } + } fn test_data() -> Vec { 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 = vec![]; + for track in album.tracks.iter() { + let track_number = track.number; + let track_title = &track.title; + + let mut track_artist: Vec = 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) -> String { + let mut albums_strings: Vec = 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 = 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 = 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); } diff --git a/src/database/mod.rs b/src/database/mod.rs index d5ab4c8..22a4067 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -8,7 +8,7 @@ pub mod json; /// Trait for database reads. pub trait DatabaseRead { /// Read collection from the database. - fn read(&mut self, collection: &mut D) -> Result<(), std::io::Error> + fn read(&self, collection: &mut D) -> Result<(), std::io::Error> where D: DeserializeOwned; } diff --git a/src/library/beets.rs b/src/library/beets.rs index 853f42a..3ce2d94 100644 --- a/src/library/beets.rs +++ b/src/library/beets.rs @@ -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) -> Result, Error>; + fn exec(&mut self, arguments: &[String]) -> Result, 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, 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) -> Result, Error> { + fn exec(&mut self, arguments: &[String]) -> Result, 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) -> Result, Error> { - if self.arguments.is_some() { - assert_eq!(self.arguments.take().unwrap(), arguments); - } + fn exec(&mut self, arguments: &[String]) -> Result, 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) -> Vec { + 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()]), diff --git a/tests/files/database_json_test.json b/tests/files/database_json_test.json deleted file mode 100644 index afbc9fb..0000000 --- a/tests/files/database_json_test.json +++ /dev/null @@ -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"]}]}] \ No newline at end of file