WIP: 93---musichoard-with-dyn-trait-or-generics #94

Closed
wojtek wants to merge 2 commits from 93---musichoard-with-dyn-trait-or-generics into main
6 changed files with 72 additions and 76 deletions

View File

@ -1,11 +1,10 @@
//! Module for storing MusicHoard data in a JSON file database. //! Module for storing MusicHoard data in a JSON file database.
use serde::de::DeserializeOwned;
use serde::Serialize;
#[cfg(test)] #[cfg(test)]
use mockall::automock; use mockall::automock;
use crate::Collection;
use super::{IDatabase, LoadError, SaveError}; use super::{IDatabase, LoadError, SaveError};
pub mod backend; pub mod backend;
@ -46,13 +45,13 @@ impl<JDB: IJsonDatabaseBackend> JsonDatabase<JDB> {
} }
impl<JDB: IJsonDatabaseBackend> IDatabase for JsonDatabase<JDB> { impl<JDB: IJsonDatabaseBackend> IDatabase for JsonDatabase<JDB> {
fn load<D: DeserializeOwned>(&self, collection: &mut D) -> Result<(), LoadError> { fn load(&self, collection: &mut Collection) -> Result<(), LoadError> {
let serialized = self.backend.read()?; let serialized = self.backend.read()?;
*collection = serde_json::from_str(&serialized)?; *collection = serde_json::from_str(&serialized)?;
Ok(()) Ok(())
} }
fn save<S: Serialize>(&mut self, collection: &S) -> Result<(), SaveError> { fn save(&mut self, collection: &Collection) -> Result<(), SaveError> {
let serialized = serde_json::to_string(&collection)?; let serialized = serde_json::to_string(&collection)?;
self.backend.write(&serialized)?; self.backend.write(&serialized)?;
Ok(()) Ok(())

View File

@ -2,11 +2,11 @@
use std::fmt; use std::fmt;
use serde::{de::DeserializeOwned, Serialize};
#[cfg(test)] #[cfg(test)]
use mockall::automock; use mockall::automock;
use crate::Collection;
#[cfg(feature = "database-json")] #[cfg(feature = "database-json")]
pub mod json; pub mod json;
@ -14,21 +14,21 @@ pub mod json;
#[cfg_attr(test, automock)] #[cfg_attr(test, automock)]
pub trait IDatabase { pub trait IDatabase {
/// Load collection from the database. /// Load collection from the database.
fn load<D: DeserializeOwned + 'static>(&self, collection: &mut D) -> Result<(), LoadError>; fn load(&self, collection: &mut Collection) -> Result<(), LoadError>;
/// Save collection to the database. /// Save collection to the database.
fn save<S: Serialize + 'static>(&mut self, collection: &S) -> Result<(), SaveError>; fn save(&mut self, collection: &Collection) -> Result<(), SaveError>;
} }
/// Non-implementation defined to make generics easier. /// Non-implementation defined to make generics easier.
pub struct NoDatabase {} pub struct NoDatabase {}
impl IDatabase for NoDatabase { impl IDatabase for NoDatabase {
fn load<D: DeserializeOwned + 'static>(&self, _collection: &mut D) -> Result<(), LoadError> { fn load(&self, _collection: &mut Collection) -> Result<(), LoadError> {
panic!() panic!()
} }
fn save<S: Serialize + 'static>(&mut self, _collection: &S) -> Result<(), SaveError> { fn save(&mut self, _collection: &Collection) -> Result<(), SaveError> {
panic!() panic!()
} }
} }
@ -89,13 +89,15 @@ impl From<std::io::Error> for SaveError {
mod tests { mod tests {
use std::io; use std::io;
use crate::Artist;
use super::{IDatabase, LoadError, NoDatabase, SaveError}; use super::{IDatabase, LoadError, NoDatabase, SaveError};
#[test] #[test]
#[should_panic] #[should_panic]
fn no_database_load() { fn no_database_load() {
let database = NoDatabase {}; let database = NoDatabase {};
let mut collection: Vec<String> = vec![]; let mut collection: Vec<Artist> = vec![];
_ = database.load(&mut collection); _ = database.load(&mut collection);
} }
@ -103,7 +105,7 @@ mod tests {
#[should_panic] #[should_panic]
fn no_database_save() { fn no_database_save() {
let mut database = NoDatabase {}; let mut database = NoDatabase {};
let collection: Vec<String> = vec![]; let collection: Vec<Artist> = vec![];
_ = database.save(&collection); _ = database.save(&collection);
} }

View File

@ -692,9 +692,9 @@ impl From<InvalidUrlError> for Error {
/// The Music Hoard. It is responsible for pulling information from both the library and the /// The Music Hoard. It is responsible for pulling information from both the library and the
/// database, ensuring its consistent and writing back any changes. /// database, ensuring its consistent and writing back any changes.
pub struct MusicHoard<LIB, DB> { pub struct MusicHoard {
library: Option<LIB>, library: Option<Box<dyn ILibrary>>,
database: Option<DB>, database: Option<Box<dyn IDatabase>>,
collection: Collection, collection: Collection,
} }
@ -773,9 +773,9 @@ macro_rules! music_hoard_multi_url_dispatch {
}; };
} }
impl<LIB: ILibrary, DB: IDatabase> MusicHoard<LIB, DB> { impl MusicHoard {
/// Create a new [`MusicHoard`] with the provided [`ILibrary`] and [`IDatabase`]. /// Create a new [`MusicHoard`] with the provided [`ILibrary`] and [`IDatabase`].
pub fn new(library: Option<LIB>, database: Option<DB>) -> Self { pub fn new(library: Option<Box<dyn ILibrary>>, database: Option<Box<dyn IDatabase>>) -> Self {
MusicHoard { MusicHoard {
library, library,
database, database,
@ -1063,7 +1063,7 @@ mod tests {
fn artist_new_delete() { fn artist_new_delete() {
let artist_id = ArtistId::new("an artist"); let artist_id = ArtistId::new("an artist");
let artist_id_2 = ArtistId::new("another artist"); let artist_id_2 = ArtistId::new("another artist");
let mut music_hoard = MusicHoard::<NoLibrary, NoDatabase>::new(None, None); let mut music_hoard = MusicHoard::new(None, None);
let mut expected: Vec<Artist> = vec![]; let mut expected: Vec<Artist> = vec![];
@ -1085,7 +1085,7 @@ mod tests {
#[test] #[test]
fn collection_error() { fn collection_error() {
let artist_id = ArtistId::new("an artist"); let artist_id = ArtistId::new("an artist");
let mut music_hoard = MusicHoard::<NoLibrary, NoDatabase>::new(None, None); let mut music_hoard = MusicHoard::new(None, None);
let actual_err = music_hoard let actual_err = music_hoard
.add_musicbrainz_url(&artist_id, QOBUZ) .add_musicbrainz_url(&artist_id, QOBUZ)
@ -1100,7 +1100,7 @@ mod tests {
fn add_remove_musicbrainz_url() { fn add_remove_musicbrainz_url() {
let artist_id = ArtistId::new("an artist"); let artist_id = ArtistId::new("an artist");
let artist_id_2 = ArtistId::new("another artist"); let artist_id_2 = ArtistId::new("another artist");
let mut music_hoard = MusicHoard::<NoLibrary, NoDatabase>::new(None, None); let mut music_hoard = MusicHoard::new(None, None);
music_hoard.add_artist(artist_id.clone()); music_hoard.add_artist(artist_id.clone());
@ -1171,7 +1171,7 @@ mod tests {
fn set_clear_musicbrainz_url() { fn set_clear_musicbrainz_url() {
let artist_id = ArtistId::new("an artist"); let artist_id = ArtistId::new("an artist");
let artist_id_2 = ArtistId::new("another artist"); let artist_id_2 = ArtistId::new("another artist");
let mut music_hoard = MusicHoard::<NoLibrary, NoDatabase>::new(None, None); let mut music_hoard = MusicHoard::new(None, None);
music_hoard.add_artist(artist_id.clone()); music_hoard.add_artist(artist_id.clone());
@ -1226,7 +1226,7 @@ mod tests {
fn add_remove_musicbutler_urls() { fn add_remove_musicbutler_urls() {
let artist_id = ArtistId::new("an artist"); let artist_id = ArtistId::new("an artist");
let artist_id_2 = ArtistId::new("another artist"); let artist_id_2 = ArtistId::new("another artist");
let mut music_hoard = MusicHoard::<NoLibrary, NoDatabase>::new(None, None); let mut music_hoard = MusicHoard::new(None, None);
music_hoard.add_artist(artist_id.clone()); music_hoard.add_artist(artist_id.clone());
@ -1356,7 +1356,7 @@ mod tests {
fn set_clear_musicbutler_urls() { fn set_clear_musicbutler_urls() {
let artist_id = ArtistId::new("an artist"); let artist_id = ArtistId::new("an artist");
let artist_id_2 = ArtistId::new("another artist"); let artist_id_2 = ArtistId::new("another artist");
let mut music_hoard = MusicHoard::<NoLibrary, NoDatabase>::new(None, None); let mut music_hoard = MusicHoard::new(None, None);
music_hoard.add_artist(artist_id.clone()); music_hoard.add_artist(artist_id.clone());
@ -1419,7 +1419,7 @@ mod tests {
fn add_remove_bandcamp_urls() { fn add_remove_bandcamp_urls() {
let artist_id = ArtistId::new("an artist"); let artist_id = ArtistId::new("an artist");
let artist_id_2 = ArtistId::new("another artist"); let artist_id_2 = ArtistId::new("another artist");
let mut music_hoard = MusicHoard::<NoLibrary, NoDatabase>::new(None, None); let mut music_hoard = MusicHoard::new(None, None);
music_hoard.add_artist(artist_id.clone()); music_hoard.add_artist(artist_id.clone());
@ -1549,7 +1549,7 @@ mod tests {
fn set_clear_bandcamp_urls() { fn set_clear_bandcamp_urls() {
let artist_id = ArtistId::new("an artist"); let artist_id = ArtistId::new("an artist");
let artist_id_2 = ArtistId::new("another artist"); let artist_id_2 = ArtistId::new("another artist");
let mut music_hoard = MusicHoard::<NoLibrary, NoDatabase>::new(None, None); let mut music_hoard = MusicHoard::new(None, None);
music_hoard.add_artist(artist_id.clone()); music_hoard.add_artist(artist_id.clone());
@ -1612,7 +1612,7 @@ mod tests {
fn add_remove_qobuz_url() { fn add_remove_qobuz_url() {
let artist_id = ArtistId::new("an artist"); let artist_id = ArtistId::new("an artist");
let artist_id_2 = ArtistId::new("another artist"); let artist_id_2 = ArtistId::new("another artist");
let mut music_hoard = MusicHoard::<NoLibrary, NoDatabase>::new(None, None); let mut music_hoard = MusicHoard::new(None, None);
music_hoard.add_artist(artist_id.clone()); music_hoard.add_artist(artist_id.clone());
@ -1663,7 +1663,7 @@ mod tests {
fn set_clear_qobuz_url() { fn set_clear_qobuz_url() {
let artist_id = ArtistId::new("an artist"); let artist_id = ArtistId::new("an artist");
let artist_id_2 = ArtistId::new("another artist"); let artist_id_2 = ArtistId::new("another artist");
let mut music_hoard = MusicHoard::<NoLibrary, NoDatabase>::new(None, None); let mut music_hoard = MusicHoard::new(None, None);
music_hoard.add_artist(artist_id.clone()); music_hoard.add_artist(artist_id.clone());
@ -1802,7 +1802,7 @@ mod tests {
let mut expected = COLLECTION.to_owned(); let mut expected = COLLECTION.to_owned();
expected.sort_unstable(); expected.sort_unstable();
let merged = MusicHoard::<MockILibrary, MockIDatabase>::merge(left.clone(), right); let merged = MusicHoard::merge(left.clone(), right);
assert_eq!(expected, merged); assert_eq!(expected, merged);
} }
@ -1816,7 +1816,7 @@ mod tests {
let mut expected = COLLECTION.to_owned(); let mut expected = COLLECTION.to_owned();
expected.sort_unstable(); expected.sort_unstable();
let merged = MusicHoard::<MockILibrary, MockIDatabase>::merge(left.clone(), right); let merged = MusicHoard::merge(left.clone(), right);
assert_eq!(expected, merged); assert_eq!(expected, merged);
} }
@ -1834,7 +1834,7 @@ mod tests {
.times(1) .times(1)
.return_once(|_| library_result); .return_once(|_| library_result);
let mut music_hoard = MusicHoard::new(Some(library), Some(database)); let mut music_hoard = MusicHoard::new(Some(Box::new(library)), Some(Box::new(database)));
music_hoard.rescan_library().unwrap(); music_hoard.rescan_library().unwrap();
assert_eq!( assert_eq!(
@ -1861,7 +1861,7 @@ mod tests {
.times(1) .times(1)
.return_once(|_| library_result); .return_once(|_| library_result);
let mut music_hoard = MusicHoard::new(Some(library), Some(database)); let mut music_hoard = MusicHoard::new(Some(Box::new(library)), Some(Box::new(database)));
music_hoard.rescan_library().unwrap(); music_hoard.rescan_library().unwrap();
assert_eq!( assert_eq!(
@ -1888,7 +1888,7 @@ mod tests {
.times(1) .times(1)
.return_once(|_| library_result); .return_once(|_| library_result);
let mut music_hoard = MusicHoard::new(Some(library), Some(database)); let mut music_hoard = MusicHoard::new(Some(Box::new(library)), Some(Box::new(database)));
music_hoard.rescan_library().unwrap(); music_hoard.rescan_library().unwrap();
assert_eq!(music_hoard.get_collection(), &expected); assert_eq!(music_hoard.get_collection(), &expected);
@ -1907,7 +1907,7 @@ mod tests {
Ok(()) Ok(())
}); });
let mut music_hoard = MusicHoard::new(Some(library), Some(database)); let mut music_hoard = MusicHoard::new(Some(Box::new(library)), Some(Box::new(database)));
music_hoard.load_from_database().unwrap(); music_hoard.load_from_database().unwrap();
assert_eq!(music_hoard.get_collection(), &*COLLECTION); assert_eq!(music_hoard.get_collection(), &*COLLECTION);
@ -1936,7 +1936,7 @@ mod tests {
.times(1) .times(1)
.return_once(|_: &Collection| database_result); .return_once(|_: &Collection| database_result);
let mut music_hoard = MusicHoard::new(Some(library), Some(database)); let mut music_hoard = MusicHoard::new(Some(Box::new(library)), Some(Box::new(database)));
music_hoard.rescan_library().unwrap(); music_hoard.rescan_library().unwrap();
assert_eq!( assert_eq!(
@ -1948,9 +1948,8 @@ mod tests {
#[test] #[test]
fn library_not_provided() { fn library_not_provided() {
let library: Option<NoLibrary> = None; let database = MockIDatabase::new();
let database = Some(MockIDatabase::new()); let mut music_hoard = MusicHoard::new(None, Some(Box::new(database)));
let mut music_hoard = MusicHoard::new(library, database);
assert!(music_hoard.rescan_library().is_ok()); assert!(music_hoard.rescan_library().is_ok());
} }
@ -1967,7 +1966,7 @@ mod tests {
.times(1) .times(1)
.return_once(|_| library_result); .return_once(|_| library_result);
let mut music_hoard = MusicHoard::new(Some(library), Some(database)); let mut music_hoard = MusicHoard::new(Some(Box::new(library)), Some(Box::new(database)));
let actual_err = music_hoard.rescan_library().unwrap_err(); let actual_err = music_hoard.rescan_library().unwrap_err();
let expected_err = let expected_err =
@ -1979,9 +1978,8 @@ mod tests {
#[test] #[test]
fn database_not_provided() { fn database_not_provided() {
let library = Some(MockILibrary::new()); let library = MockILibrary::new();
let database: Option<NoDatabase> = None; let mut music_hoard = MusicHoard::new(Some(Box::new(library)), None);
let mut music_hoard = MusicHoard::new(library, database);
assert!(music_hoard.load_from_database().is_ok()); assert!(music_hoard.load_from_database().is_ok());
assert!(music_hoard.save_to_database().is_ok()); assert!(music_hoard.save_to_database().is_ok());
@ -1999,7 +1997,7 @@ mod tests {
.times(1) .times(1)
.return_once(|_: &mut Collection| database_result); .return_once(|_: &mut Collection| database_result);
let mut music_hoard = MusicHoard::new(Some(library), Some(database)); let mut music_hoard = MusicHoard::new(Some(Box::new(library)), Some(Box::new(database)));
let actual_err = music_hoard.load_from_database().unwrap_err(); let actual_err = music_hoard.load_from_database().unwrap_err();
let expected_err = Error::DatabaseError( let expected_err = Error::DatabaseError(
@ -2022,7 +2020,7 @@ mod tests {
.times(1) .times(1)
.return_once(|_: &Collection| database_result); .return_once(|_: &Collection| database_result);
let mut music_hoard = MusicHoard::new(Some(library), Some(database)); let mut music_hoard = MusicHoard::new(Some(Box::new(library)), Some(Box::new(database)));
let actual_err = music_hoard.save_to_database().unwrap_err(); let actual_err = music_hoard.save_to_database().unwrap_err();
let expected_err = Error::DatabaseError( let expected_err = Error::DatabaseError(

View File

@ -2,9 +2,6 @@ use std::fs::OpenOptions;
use std::path::PathBuf; use std::path::PathBuf;
use std::{ffi::OsString, io}; use std::{ffi::OsString, io};
use musichoard::database::NoDatabase;
use musichoard::library::NoLibrary;
use musichoard::Collection;
use ratatui::{backend::CrosstermBackend, Terminal}; use ratatui::{backend::CrosstermBackend, Terminal};
use structopt::StructOpt; use structopt::StructOpt;
@ -49,7 +46,7 @@ struct Opt {
no_database: bool, no_database: bool,
} }
fn with<LIB: ILibrary, DB: IDatabase>(lib: Option<LIB>, db: Option<DB>) { fn with(lib: Option<Box<dyn ILibrary>>, db: Option<Box<dyn IDatabase>>) {
let music_hoard = MusicHoard::new(lib, db); let music_hoard = MusicHoard::new(lib, db);
// Initialize the terminal user interface. // Initialize the terminal user interface.
@ -66,31 +63,11 @@ fn with<LIB: ILibrary, DB: IDatabase>(lib: Option<LIB>, db: Option<DB>) {
Tui::run(terminal, ui, handler, listener).expect("failed to run tui"); Tui::run(terminal, ui, handler, listener).expect("failed to run tui");
} }
fn with_database<DB: IDatabase>(opt: Opt, db: Option<DB>) {
if opt.no_library {
with(None::<NoLibrary>, db);
} else if let Some(uri) = opt.beets_ssh_uri {
let uri = uri.into_string().expect("invalid SSH URI");
let beets_config_file_path = opt
.beets_config_file_path
.map(|s| s.into_string())
.transpose()
.expect("failed to extract beets config file path");
let lib_exec = BeetsLibrarySshExecutor::new(uri)
.expect("failed to initialise beets")
.config(beets_config_file_path);
with(Some(BeetsLibrary::new(lib_exec)), db);
} else {
let lib_exec = BeetsLibraryProcessExecutor::default().config(opt.beets_config_file_path);
with(Some(BeetsLibrary::new(lib_exec)), db);
}
}
fn main() { fn main() {
let opt = Opt::from_args(); let opt = Opt::from_args();
if opt.no_database { let database: Option<Box<dyn IDatabase>> = if opt.no_database {
with_database(opt, None::<NoDatabase>); None
} else { } else {
// Create an empty database file if it does not exist. // Create an empty database file if it does not exist.
match OpenOptions::new() match OpenOptions::new()
@ -101,7 +78,7 @@ fn main() {
Ok(f) => { Ok(f) => {
drop(f); drop(f);
JsonDatabase::new(JsonDatabaseFileBackend::new(&opt.database_file_path)) JsonDatabase::new(JsonDatabaseFileBackend::new(&opt.database_file_path))
.save::<Collection>(&vec![]) .save(&vec![])
.expect("failed to create empty database"); .expect("failed to create empty database");
} }
Err(e) => match e.kind() { Err(e) => match e.kind() {
@ -111,8 +88,28 @@ fn main() {
} }
let db_exec = JsonDatabaseFileBackend::new(&opt.database_file_path); let db_exec = JsonDatabaseFileBackend::new(&opt.database_file_path);
with_database(opt, Some(JsonDatabase::new(db_exec))); Some(Box::new(JsonDatabase::new(db_exec)))
}; };
let library: Option<Box<dyn ILibrary>> = if opt.no_library {
None
} else if let Some(uri) = opt.beets_ssh_uri {
let uri = uri.into_string().expect("invalid SSH URI");
let beets_config_file_path = opt
.beets_config_file_path
.map(|s| s.into_string())
.transpose()
.expect("failed to extract beets config file path");
let lib_exec = BeetsLibrarySshExecutor::new(uri)
.expect("failed to initialise beets")
.config(beets_config_file_path);
Some(Box::new(BeetsLibrary::new(lib_exec)))
} else {
let lib_exec = BeetsLibraryProcessExecutor::default().config(opt.beets_config_file_path);
Some(Box::new(BeetsLibrary::new(lib_exec)))
};
with(library, database);
} }
#[cfg(test)] #[cfg(test)]

View File

@ -1,4 +1,4 @@
use musichoard::{database::IDatabase, library::ILibrary, Collection, MusicHoard}; use musichoard::{Collection, MusicHoard};
#[cfg(test)] #[cfg(test)]
use mockall::automock; use mockall::automock;
@ -12,7 +12,7 @@ pub trait IMusicHoard {
} }
// GRCOV_EXCL_START // GRCOV_EXCL_START
impl<LIB: ILibrary, DB: IDatabase> IMusicHoard for MusicHoard<LIB, DB> { impl IMusicHoard for MusicHoard {
fn rescan_library(&mut self) -> Result<(), musichoard::Error> { fn rescan_library(&mut self) -> Result<(), musichoard::Error> {
MusicHoard::rescan_library(self) MusicHoard::rescan_library(self)
} }

View File

@ -30,7 +30,7 @@ fn merge_library_then_database() {
let backend = JsonDatabaseFileBackend::new(&*database::json::DATABASE_TEST_FILE); let backend = JsonDatabaseFileBackend::new(&*database::json::DATABASE_TEST_FILE);
let database = JsonDatabase::new(backend); let database = JsonDatabase::new(backend);
let mut music_hoard = MusicHoard::new(Some(library), Some(database)); let mut music_hoard = MusicHoard::new(Some(Box::new(library)), Some(Box::new(database)));
music_hoard.rescan_library().unwrap(); music_hoard.rescan_library().unwrap();
music_hoard.load_from_database().unwrap(); music_hoard.load_from_database().unwrap();
@ -55,7 +55,7 @@ fn merge_database_then_library() {
let backend = JsonDatabaseFileBackend::new(&*database::json::DATABASE_TEST_FILE); let backend = JsonDatabaseFileBackend::new(&*database::json::DATABASE_TEST_FILE);
let database = JsonDatabase::new(backend); let database = JsonDatabase::new(backend);
let mut music_hoard = MusicHoard::new(Some(library), Some(database)); let mut music_hoard = MusicHoard::new(Some(Box::new(library)), Some(Box::new(database)));
music_hoard.load_from_database().unwrap(); music_hoard.load_from_database().unwrap();
music_hoard.rescan_library().unwrap(); music_hoard.rescan_library().unwrap();