From 282e0e6f1940abb0c2cc50e6a20817a101f3a34c Mon Sep 17 00:00:00 2001 From: Wojciech Kozlowski Date: Wed, 10 May 2023 22:52:03 +0200 Subject: [PATCH] Clean up interfaces (#62) Closes #61 Reviewed-on: https://git.wojciechkozlowski.eu/wojtek/musichoard/pulls/62 --- README.md | 2 +- src/collection/mod.rs | 178 ---------------------------------- src/database/json/backend.rs | 4 +- src/database/json/mod.rs | 14 +-- src/database/mod.rs | 20 ++-- src/lib.rs | 156 ++++++++++++++++++++++++++++- src/library/beets/executor.rs | 12 +-- src/library/beets/mod.rs | 26 ++--- src/library/mod.rs | 14 +-- src/main.rs | 32 ++---- src/tui/handler.rs | 16 +-- src/tui/lib.rs | 30 ++++++ src/tui/listener.rs | 10 +- src/tui/mod.rs | 92 ++++++++---------- src/tui/ui.rs | 106 ++++++++++---------- tests/database/json.rs | 2 +- tests/library/beets.rs | 2 +- 17 files changed, 344 insertions(+), 372 deletions(-) delete mode 100644 src/collection/mod.rs create mode 100644 src/tui/lib.rs diff --git a/README.md b/README.md index 49ac8bb..3535a34 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ env CARGO_TARGET_DIR=codecov \ env RUSTFLAGS="-C instrument-coverage" \ LLVM_PROFILE_FILE="codecov/debug/profraw/musichoard-%p-%m.profraw" \ CARGO_TARGET_DIR=codecov \ - cargo test --all-features + cargo test --all-features --all-targets grcov codecov/debug/profraw \ --binary-path ./codecov/debug/ \ --output-types html \ diff --git a/src/collection/mod.rs b/src/collection/mod.rs deleted file mode 100644 index b75f4a1..0000000 --- a/src/collection/mod.rs +++ /dev/null @@ -1,178 +0,0 @@ -//! Module for managing the music collection, i.e. "The Music Hoard". - -use std::fmt; - -use crate::{ - database::{self, Database}, - library::{self, Library, Query}, - Artist, -}; - -/// The collection type. -pub type Collection = Vec; - -/// Error type for collection manager. -#[derive(Debug, PartialEq, Eq)] -pub enum Error { - /// The [`CollectionManager`] failed to read/write from/to the library. - LibraryError(String), - /// The [`CollectionManager`] failed to read/write from/to the database. - DatabaseError(String), -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - Self::LibraryError(ref s) => write!(f, "failed to read/write from/to the library: {s}"), - Self::DatabaseError(ref s) => { - write!(f, "failed to read/write from/to the database: {s}") - } - } - } -} - -impl From for Error { - fn from(err: library::Error) -> Error { - Error::LibraryError(err.to_string()) - } -} - -impl From for Error { - fn from(err: database::Error) -> Error { - Error::DatabaseError(err.to_string()) - } -} - -pub trait CollectionManager { - /// Rescan the library and integrate any updates into the collection. - fn rescan_library(&mut self) -> Result<(), Error>; - - /// Save the collection state to the database. - fn save_to_database(&mut self) -> Result<(), Error>; - - /// Get the current collection. - fn get_collection(&self) -> &Collection; -} - -/// The collection manager. It is responsible for pulling information from both the library and the -/// database, ensuring its consistent and writing back any changes. -pub struct MhCollectionManager { - library: LIB, - database: DB, - collection: Collection, -} - -impl MhCollectionManager { - /// Create a new [`CollectionManager`] with the provided [`Library`] and [`Database`]. - pub fn new(library: LIB, database: DB) -> Self { - MhCollectionManager { - library, - database, - collection: vec![], - } - } -} - -impl CollectionManager for MhCollectionManager { - fn rescan_library(&mut self) -> Result<(), Error> { - self.collection = self.library.list(&Query::new())?; - Ok(()) - } - - fn save_to_database(&mut self) -> Result<(), Error> { - self.database.write(&self.collection)?; - Ok(()) - } - - fn get_collection(&self) -> &Collection { - &self.collection - } -} - -#[cfg(test)] -mod tests { - use mockall::predicate; - - use crate::{ - collection::Collection, - database::{self, MockDatabase}, - library::{self, MockLibrary, Query}, - tests::COLLECTION, - }; - - use super::{CollectionManager, Error, MhCollectionManager}; - - #[test] - fn read_get_write() { - let mut library = MockLibrary::new(); - let mut database = MockDatabase::new(); - - let library_input = Query::new(); - let library_result = Ok(COLLECTION.to_owned()); - - let database_input = COLLECTION.to_owned(); - let database_result = Ok(()); - - library - .expect_list() - .with(predicate::eq(library_input)) - .times(1) - .return_once(|_| library_result); - - database - .expect_write() - .with(predicate::eq(database_input)) - .times(1) - .return_once(|_: &Collection| database_result); - - let mut collection_manager = MhCollectionManager::new(library, database); - - collection_manager.rescan_library().unwrap(); - assert_eq!(collection_manager.get_collection(), &*COLLECTION); - collection_manager.save_to_database().unwrap(); - } - - #[test] - fn library_error() { - let mut library = MockLibrary::new(); - let database = MockDatabase::new(); - - let library_result = Err(library::Error::Invalid(String::from("invalid data"))); - - library - .expect_list() - .times(1) - .return_once(|_| library_result); - - let mut collection_manager = MhCollectionManager::new(library, database); - - let actual_err = collection_manager.rescan_library().unwrap_err(); - let expected_err = - Error::LibraryError(library::Error::Invalid(String::from("invalid data")).to_string()); - - assert_eq!(actual_err, expected_err); - assert_eq!(actual_err.to_string(), expected_err.to_string()); - } - - #[test] - fn database_error() { - let library = MockLibrary::new(); - let mut database = MockDatabase::new(); - - let database_result = Err(database::Error::IoError(String::from("I/O error"))); - - database - .expect_write() - .times(1) - .return_once(|_: &Collection| database_result); - - let mut collection_manager = MhCollectionManager::new(library, database); - - let actual_err = collection_manager.save_to_database().unwrap_err(); - let expected_err = - Error::DatabaseError(database::Error::IoError(String::from("I/O error")).to_string()); - - assert_eq!(actual_err, expected_err); - assert_eq!(actual_err.to_string(), expected_err.to_string()); - } -} diff --git a/src/database/json/backend.rs b/src/database/json/backend.rs index 32026d1..d644fe4 100644 --- a/src/database/json/backend.rs +++ b/src/database/json/backend.rs @@ -3,7 +3,7 @@ use std::fs; use std::path::PathBuf; -use super::JsonDatabaseBackend; +use super::IJsonDatabaseBackend; /// JSON database backend that uses a local file for persistent storage. pub struct JsonDatabaseFileBackend { @@ -17,7 +17,7 @@ impl JsonDatabaseFileBackend { } } -impl JsonDatabaseBackend for JsonDatabaseFileBackend { +impl IJsonDatabaseBackend for JsonDatabaseFileBackend { 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 diff --git a/src/database/json/mod.rs b/src/database/json/mod.rs index 0398292..32abaa0 100644 --- a/src/database/json/mod.rs +++ b/src/database/json/mod.rs @@ -6,7 +6,7 @@ use serde::Serialize; #[cfg(test)] use mockall::automock; -use super::{Database, Error}; +use super::{Error, IDatabase}; pub mod backend; @@ -18,7 +18,7 @@ impl From for Error { /// Trait for the JSON database backend. #[cfg_attr(test, automock)] -pub trait JsonDatabaseBackend { +pub trait IJsonDatabaseBackend { /// Read the JSON string from the backend. fn read(&self) -> Result; @@ -31,7 +31,7 @@ pub struct JsonDatabase { backend: JDB, } -impl JsonDatabase { +impl JsonDatabase { /// Create a new JSON database with the provided backend, e.g. /// [`backend::JsonDatabaseFileBackend`]. pub fn new(backend: JDB) -> Self { @@ -39,7 +39,7 @@ impl JsonDatabase { } } -impl Database for JsonDatabase { +impl IDatabase for JsonDatabase { fn read(&self, collection: &mut D) -> Result<(), Error> { let serialized = self.backend.read()?; *collection = serde_json::from_str(&serialized)?; @@ -132,7 +132,7 @@ mod tests { let write_data = COLLECTION.to_owned(); let input = artists_to_json(&write_data); - let mut backend = MockJsonDatabaseBackend::new(); + let mut backend = MockIJsonDatabaseBackend::new(); backend .expect_write() .with(predicate::eq(input)) @@ -147,7 +147,7 @@ mod tests { let expected = COLLECTION.to_owned(); let result = Ok(artists_to_json(&expected)); - let mut backend = MockJsonDatabaseBackend::new(); + let mut backend = MockIJsonDatabaseBackend::new(); backend.expect_read().times(1).return_once(|| result); let mut read_data: Vec = vec![]; @@ -162,7 +162,7 @@ mod tests { let input = artists_to_json(&expected); let result = Ok(input.clone()); - let mut backend = MockJsonDatabaseBackend::new(); + let mut backend = MockIJsonDatabaseBackend::new(); backend .expect_write() .with(predicate::eq(input)) diff --git a/src/database/mod.rs b/src/database/mod.rs index 8434b7b..91bc687 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -10,6 +10,16 @@ use mockall::automock; #[cfg(feature = "database-json")] pub mod json; +/// Trait for interacting with the database. +#[cfg_attr(test, automock)] +pub trait IDatabase { + /// Read collection from the database. + fn read(&self, collection: &mut D) -> Result<(), Error>; + + /// Write collection to the database. + fn write(&mut self, collection: &S) -> Result<(), Error>; +} + /// Error type for database calls. #[derive(Debug)] pub enum Error { @@ -36,16 +46,6 @@ impl From for Error { } } -/// Trait for interacting with the database. -#[cfg_attr(test, automock)] -pub trait Database { - /// Read collection from the database. - fn read(&self, collection: &mut D) -> Result<(), Error>; - - /// Write collection to the database. - fn write(&mut self, collection: &S) -> Result<(), Error>; -} - #[cfg(test)] mod tests { use std::io; diff --git a/src/lib.rs b/src/lib.rs index 06a807b..768fab4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,12 +1,15 @@ //! MusicHoard - a music collection manager. -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -pub mod collection; pub mod database; pub mod library; +use std::fmt; + +use database::IDatabase; +use library::{ILibrary, Query}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + /// [MusicBrainz Identifier](https://musicbrainz.org/doc/MusicBrainz_Identifier) (MBID). pub type Mbid = Uuid; @@ -53,15 +56,160 @@ pub struct Artist { pub albums: Vec, } +/// The collection type. Currently, a collection is a list of artists. +pub type Collection = Vec; + +/// Error type for `musichoard`. +#[derive(Debug, PartialEq, Eq)] +pub enum Error { + /// The [`MusicHoard`] failed to read/write from/to the library. + LibraryError(String), + /// The [`MusicHoard`] failed to read/write from/to the database. + DatabaseError(String), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Self::LibraryError(ref s) => write!(f, "failed to read/write from/to the library: {s}"), + Self::DatabaseError(ref s) => { + write!(f, "failed to read/write from/to the database: {s}") + } + } + } +} + +impl From for Error { + fn from(err: library::Error) -> Error { + Error::LibraryError(err.to_string()) + } +} + +impl From for Error { + fn from(err: database::Error) -> Error { + Error::DatabaseError(err.to_string()) + } +} + +/// The Music Hoard. It is responsible for pulling information from both the library and the +/// database, ensuring its consistent and writing back any changes. +pub struct MusicHoard { + library: LIB, + database: DB, + collection: Collection, +} + +impl MusicHoard { + /// Create a new [`MusicHoard`] with the provided [`ILibrary`] and [`IDatabase`]. + pub fn new(library: LIB, database: DB) -> Self { + MusicHoard { + library, + database, + collection: vec![], + } + } + + pub fn rescan_library(&mut self) -> Result<(), Error> { + self.collection = self.library.list(&Query::new())?; + Ok(()) + } + + pub fn save_to_database(&mut self) -> Result<(), Error> { + self.database.write(&self.collection)?; + Ok(()) + } + + pub fn get_collection(&self) -> &Collection { + &self.collection + } +} + #[cfg(test)] #[macro_use] mod testlib; #[cfg(test)] mod tests { + use mockall::predicate; use once_cell::sync::Lazy; + use crate::{database::MockIDatabase, library::MockILibrary}; + use super::*; pub static COLLECTION: Lazy> = Lazy::new(|| collection!()); + + #[test] + fn read_get_write() { + let mut library = MockILibrary::new(); + let mut database = MockIDatabase::new(); + + let library_input = Query::new(); + let library_result = Ok(COLLECTION.to_owned()); + + let database_input = COLLECTION.to_owned(); + let database_result = Ok(()); + + library + .expect_list() + .with(predicate::eq(library_input)) + .times(1) + .return_once(|_| library_result); + + database + .expect_write() + .with(predicate::eq(database_input)) + .times(1) + .return_once(|_: &Collection| database_result); + + let mut music_hoard = MusicHoard::new(library, database); + + music_hoard.rescan_library().unwrap(); + assert_eq!(music_hoard.get_collection(), &*COLLECTION); + music_hoard.save_to_database().unwrap(); + } + + #[test] + fn library_error() { + let mut library = MockILibrary::new(); + let database = MockIDatabase::new(); + + let library_result = Err(library::Error::Invalid(String::from("invalid data"))); + + library + .expect_list() + .times(1) + .return_once(|_| library_result); + + let mut music_hoard = MusicHoard::new(library, database); + + let actual_err = music_hoard.rescan_library().unwrap_err(); + let expected_err = + Error::LibraryError(library::Error::Invalid(String::from("invalid data")).to_string()); + + assert_eq!(actual_err, expected_err); + assert_eq!(actual_err.to_string(), expected_err.to_string()); + } + + #[test] + fn database_error() { + let library = MockILibrary::new(); + let mut database = MockIDatabase::new(); + + let database_result = Err(database::Error::IoError(String::from("I/O error"))); + + database + .expect_write() + .times(1) + .return_once(|_: &Collection| database_result); + + let mut music_hoard = MusicHoard::new(library, database); + + let actual_err = music_hoard.save_to_database().unwrap_err(); + let expected_err = + Error::DatabaseError(database::Error::IoError(String::from("I/O error")).to_string()); + + assert_eq!(actual_err, expected_err); + assert_eq!(actual_err.to_string(), expected_err.to_string()); + } } diff --git a/src/library/beets/executor.rs b/src/library/beets/executor.rs index 7d177b9..434b89c 100644 --- a/src/library/beets/executor.rs +++ b/src/library/beets/executor.rs @@ -8,11 +8,11 @@ use std::{ str, }; -use super::{BeetsLibraryExecutor, Error}; +use super::{IBeetsLibraryExecutor, Error}; const BEET_DEFAULT: &str = "beet"; -trait BeetsLibraryExecutorPrivate { +trait IBeetsLibraryExecutorPrivate { fn output(output: Output) -> Result, Error> { if !output.status.success() { return Err(Error::Executor( @@ -59,7 +59,7 @@ impl Default for BeetsLibraryProcessExecutor { } } -impl BeetsLibraryExecutor for BeetsLibraryProcessExecutor { +impl IBeetsLibraryExecutor for BeetsLibraryProcessExecutor { fn exec + 'static>(&mut self, arguments: &[S]) -> Result, Error> { let mut cmd = Command::new(&self.bin); if let Some(ref path) = self.config { @@ -71,7 +71,7 @@ impl BeetsLibraryExecutor for BeetsLibraryProcessExecutor { } } -impl BeetsLibraryExecutorPrivate for BeetsLibraryProcessExecutor {} +impl IBeetsLibraryExecutorPrivate for BeetsLibraryProcessExecutor {} // GRCOV_EXCL_START #[cfg(feature = "ssh-library")] @@ -128,7 +128,7 @@ pub mod ssh { } } - impl BeetsLibraryExecutor for BeetsLibrarySshExecutor { + impl IBeetsLibraryExecutor for BeetsLibrarySshExecutor { fn exec + 'static>(&mut self, arguments: &[S]) -> Result, Error> { let mut cmd = self.session.command(&self.bin); if let Some(ref path) = self.config { @@ -141,6 +141,6 @@ pub mod ssh { } } - impl BeetsLibraryExecutorPrivate for BeetsLibrarySshExecutor {} + impl IBeetsLibraryExecutorPrivate for BeetsLibrarySshExecutor {} } // GRCOV_EXCL_STOP diff --git a/src/library/beets/mod.rs b/src/library/beets/mod.rs index d066ea0..eccfb49 100644 --- a/src/library/beets/mod.rs +++ b/src/library/beets/mod.rs @@ -11,7 +11,7 @@ use mockall::automock; use crate::{Album, AlbumId, Artist, ArtistId, Track, TrackFormat}; -use super::{Error, Field, Library, Query}; +use super::{Error, Field, ILibrary, Query}; pub mod executor; @@ -78,7 +78,7 @@ impl ToBeetsArgs for Query { /// Trait for invoking beets commands. #[cfg_attr(test, automock)] -pub trait BeetsLibraryExecutor { +pub trait IBeetsLibraryExecutor { /// Invoke beets with the provided arguments. fn exec + 'static>(&mut self, arguments: &[S]) -> Result, Error>; } @@ -88,12 +88,12 @@ pub struct BeetsLibrary { executor: BLE, } -trait LibraryPrivate { +trait ILibraryPrivate { fn list_cmd_and_args(query: &Query) -> Vec; fn list_to_artists>(list_output: &[S]) -> Result, Error>; } -impl BeetsLibrary { +impl BeetsLibrary { /// Create a new beets library with the provided executor, e.g. /// [`executor::BeetsLibraryProcessExecutor`]. pub fn new(executor: BLE) -> Self { @@ -101,7 +101,7 @@ impl BeetsLibrary { } } -impl Library for BeetsLibrary { +impl ILibrary for BeetsLibrary { fn list(&mut self, query: &Query) -> Result, Error> { let cmd = Self::list_cmd_and_args(query); let output = self.executor.exec(&cmd)?; @@ -109,7 +109,7 @@ impl Library for BeetsLibrary { } } -impl LibraryPrivate for BeetsLibrary { +impl ILibraryPrivate for BeetsLibrary { fn list_cmd_and_args(query: &Query) -> Vec { let mut cmd: Vec = vec![String::from(CMD_LIST)]; cmd.push(LIST_FORMAT_ARG.to_string()); @@ -293,7 +293,7 @@ mod tests { let arguments = vec!["ls".to_string(), LIST_FORMAT_ARG.to_string()]; let result = Ok(vec![]); - let mut executor = MockBeetsLibraryExecutor::new(); + let mut executor = MockIBeetsLibraryExecutor::new(); executor .expect_exec() .with(predicate::eq(arguments)) @@ -313,7 +313,7 @@ mod tests { let expected = COLLECTION.to_owned(); let result = Ok(artists_to_beets_string(&expected)); - let mut executor = MockBeetsLibraryExecutor::new(); + let mut executor = MockIBeetsLibraryExecutor::new(); executor .expect_exec() .with(predicate::eq(arguments)) @@ -348,7 +348,7 @@ mod tests { // track comes last. expected[1].albums[0].tracks.rotate_left(1); - let mut executor = MockBeetsLibraryExecutor::new(); + let mut executor = MockIBeetsLibraryExecutor::new(); executor .expect_exec() .with(predicate::eq(arguments)) @@ -370,7 +370,7 @@ mod tests { let output = artists_to_beets_string(&expected); let result = Ok(output); - let mut executor = MockBeetsLibraryExecutor::new(); + let mut executor = MockIBeetsLibraryExecutor::new(); executor .expect_exec() .with(predicate::eq(arguments)) @@ -400,7 +400,7 @@ mod tests { ]; let result = Ok(vec![]); - let mut executor = MockBeetsLibraryExecutor::new(); + let mut executor = MockIBeetsLibraryExecutor::new(); executor .expect_exec() .with(predicate::function(move |x: &[String]| { @@ -431,7 +431,7 @@ mod tests { output[2] = invalid_string.clone(); let result = Ok(output); - let mut executor = MockBeetsLibraryExecutor::new(); + let mut executor = MockIBeetsLibraryExecutor::new(); executor .expect_exec() .with(predicate::eq(arguments)) @@ -462,7 +462,7 @@ mod tests { output[2] = invalid_string.clone(); let result = Ok(output); - let mut executor = MockBeetsLibraryExecutor::new(); + let mut executor = MockIBeetsLibraryExecutor::new(); executor .expect_exec() .with(predicate::eq(arguments)) diff --git a/src/library/mod.rs b/src/library/mod.rs index e73eb14..506e54e 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -10,6 +10,13 @@ use crate::Artist; #[cfg(feature = "library-beets")] pub mod beets; +/// Trait for interacting with the music library. +#[cfg_attr(test, automock)] +pub trait ILibrary { + /// List lirbary items that match the a specific query. + fn list(&mut self, query: &Query) -> Result, Error>; +} + /// Individual fields that can be queried on. #[derive(Debug, Hash, PartialEq, Eq)] pub enum Field { @@ -103,13 +110,6 @@ impl From for Error { } } -/// Trait for interacting with the music library. -#[cfg_attr(test, automock)] -pub trait Library { - /// List lirbary items that match the a specific query. - fn list(&mut self, query: &Query) -> Result, Error>; -} - #[cfg(test)] mod tests { use std::io; diff --git a/src/main.rs b/src/main.rs index 5e9c426..143d78c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,23 +5,23 @@ use ratatui::{backend::CrosstermBackend, Terminal}; use structopt::StructOpt; use musichoard::{ - collection::MhCollectionManager, database::{ json::{backend::JsonDatabaseFileBackend, JsonDatabase}, - Database, + IDatabase, }, library::{ beets::{ executor::{ssh::BeetsLibrarySshExecutor, BeetsLibraryProcessExecutor}, BeetsLibrary, }, - Library, + ILibrary, }, + MusicHoard, }; mod tui; -use tui::ui::MhUi; -use tui::{event::EventChannel, handler::TuiEventHandler, listener::TuiEventListener, Tui}; +use tui::ui::Ui; +use tui::{event::EventChannel, handler::EventHandler, listener::EventListener, Tui}; #[derive(StructOpt)] struct Opt { @@ -39,18 +39,18 @@ struct Opt { database_file_path: PathBuf, } -fn with(lib: LIB, db: DB) { - let collection_manager = MhCollectionManager::new(lib, db); +fn with(lib: LIB, db: DB) { + let music_hoard = MusicHoard::new(lib, db); // Initialize the terminal user interface. let backend = CrosstermBackend::new(io::stdout()); let terminal = Terminal::new(backend).expect("failed to initialise terminal"); let channel = EventChannel::new(); - let listener = TuiEventListener::new(channel.sender()); - let handler = TuiEventHandler::new(channel.receiver()); + let listener = EventListener::new(channel.sender()); + let handler = EventHandler::new(channel.receiver()); - let ui = MhUi::new(collection_manager).expect("failed to initialise ui"); + let ui = Ui::new(music_hoard).expect("failed to initialise ui"); // Run the TUI application. Tui::run(terminal, ui, handler, listener).expect("failed to run tui"); @@ -85,21 +85,9 @@ mod testlib; #[cfg(test)] mod tests { - use mockall::mock; use once_cell::sync::Lazy; - use musichoard::collection::{self, Collection, CollectionManager}; use musichoard::*; pub static COLLECTION: Lazy> = Lazy::new(|| collection!()); - - mock! { - pub CollectionManager {} - - impl CollectionManager for CollectionManager { - fn rescan_library(&mut self) -> Result<(), collection::Error>; - fn save_to_database(&mut self) -> Result<(), collection::Error>; - fn get_collection(&self) -> &Collection; - } - } } diff --git a/src/tui/handler.rs b/src/tui/handler.rs index b9346f6..41de726 100644 --- a/src/tui/handler.rs +++ b/src/tui/handler.rs @@ -5,30 +5,30 @@ use mockall::automock; use super::{ event::{Event, EventError, EventReceiver}, - ui::Ui, + ui::IUi, }; #[cfg_attr(test, automock)] -pub trait EventHandler { +pub trait IEventHandler { fn handle_next_event(&self, ui: &mut UI) -> Result<(), EventError>; } -trait EventHandlerPrivate { +trait IEventHandlerPrivate { fn handle_key_event(ui: &mut UI, key_event: KeyEvent); } -pub struct TuiEventHandler { +pub struct EventHandler { events: EventReceiver, } // GRCOV_EXCL_START -impl TuiEventHandler { +impl EventHandler { pub fn new(events: EventReceiver) -> Self { - TuiEventHandler { events } + EventHandler { events } } } -impl EventHandler for TuiEventHandler { +impl IEventHandler for EventHandler { fn handle_next_event(&self, ui: &mut UI) -> Result<(), EventError> { match self.events.recv()? { Event::Key(key_event) => Self::handle_key_event(ui, key_event), @@ -39,7 +39,7 @@ impl EventHandler for TuiEventHandler { } } -impl EventHandlerPrivate for TuiEventHandler { +impl IEventHandlerPrivate for EventHandler { fn handle_key_event(ui: &mut UI, key_event: KeyEvent) { match key_event.code { // Exit application on `ESC` or `q`. diff --git a/src/tui/lib.rs b/src/tui/lib.rs new file mode 100644 index 0000000..89a6242 --- /dev/null +++ b/src/tui/lib.rs @@ -0,0 +1,30 @@ +use musichoard::{database::IDatabase, library::ILibrary, Collection, MusicHoard}; + +#[cfg(test)] +use mockall::automock; + +use super::Error; + +#[cfg_attr(test, automock)] +pub trait IMusicHoard { + fn rescan_library(&mut self) -> Result<(), Error>; + fn get_collection(&self) -> &Collection; +} + +impl From for Error { + fn from(err: musichoard::Error) -> Error { + Error::Lib(err.to_string()) + } +} + +// GRCOV_EXCL_START +impl IMusicHoard for MusicHoard { + fn rescan_library(&mut self) -> Result<(), Error> { + Ok(MusicHoard::rescan_library(self)?) + } + + fn get_collection(&self) -> &Collection { + MusicHoard::get_collection(self) + } +} +// GRCOV_EXCL_STOP diff --git a/src/tui/listener.rs b/src/tui/listener.rs index 53c78da..404c03e 100644 --- a/src/tui/listener.rs +++ b/src/tui/listener.rs @@ -7,22 +7,22 @@ use mockall::automock; use super::event::{Event, EventError, EventSender}; #[cfg_attr(test, automock)] -pub trait EventListener { +pub trait IEventListener { fn spawn(self) -> thread::JoinHandle; } -pub struct TuiEventListener { +pub struct EventListener { events: EventSender, } // GRCOV_EXCL_START -impl TuiEventListener { +impl EventListener { pub fn new(events: EventSender) -> Self { - TuiEventListener { events } + EventListener { events } } } -impl EventListener for TuiEventListener { +impl IEventListener for EventListener { fn spawn(self) -> thread::JoinHandle { thread::spawn(move || { loop { diff --git a/src/tui/mod.rs b/src/tui/mod.rs index 50478b6..8e99637 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -1,35 +1,30 @@ -use crossterm::event::{DisableMouseCapture, EnableMouseCapture}; -use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}; -use musichoard::collection; -use ratatui::backend::Backend; -use ratatui::Terminal; -use std::io; -use std::marker::PhantomData; - pub mod event; pub mod handler; pub mod listener; pub mod ui; +mod lib; + +use crossterm::event::{DisableMouseCapture, EnableMouseCapture}; +use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}; +use ratatui::backend::Backend; +use ratatui::Terminal; +use std::io; +use std::marker::PhantomData; + use self::event::EventError; -use self::handler::EventHandler; -use self::listener::EventListener; -use self::ui::Ui; +use self::handler::IEventHandler; +use self::listener::IEventListener; +use self::ui::IUi; #[derive(Debug, PartialEq, Eq)] pub enum Error { - Collection(String), + Lib(String), Io(String), Event(String), ListenerPanic, } -impl From for Error { - fn from(err: collection::Error) -> Error { - Error::Collection(err.to_string()) - } -} - impl From for Error { fn from(err: io::Error) -> Error { Error::Io(err.to_string()) @@ -47,7 +42,7 @@ pub struct Tui { _phantom: PhantomData, } -impl Tui { +impl Tui { fn init(&mut self) -> Result<(), Error> { self.terminal.hide_cursor()?; self.terminal.clear()?; @@ -64,7 +59,7 @@ impl Tui { self.exit(); } - fn main_loop(&mut self, mut ui: UI, handler: impl EventHandler) -> Result<(), Error> { + fn main_loop(&mut self, mut ui: UI, handler: impl IEventHandler) -> Result<(), Error> { while ui.is_running() { self.terminal.draw(|frame| ui.render(frame))?; handler.handle_next_event(&mut ui)?; @@ -76,8 +71,8 @@ impl Tui { fn main( term: Terminal, ui: UI, - handler: impl EventHandler, - listener: impl EventListener, + handler: impl IEventHandler, + listener: impl IEventListener, ) -> Result<(), Error> { let mut tui = Tui { terminal: term, @@ -135,8 +130,8 @@ impl Tui { pub fn run( term: Terminal, ui: UI, - handler: impl EventHandler, - listener: impl EventListener, + handler: impl IEventHandler, + listener: impl IEventListener, ) -> Result<(), Error> { Self::enable()?; let result = Self::main(term, ui, handler, listener); @@ -160,16 +155,17 @@ impl Tui { mod tests { use std::{io, thread}; - use musichoard::collection::{self, Collection}; + use musichoard::Collection; use ratatui::{backend::TestBackend, Terminal}; - use crate::tests::{MockCollectionManager, COLLECTION}; + use crate::tests::COLLECTION; use super::{ event::EventError, - handler::MockEventHandler, - listener::MockEventListener, - ui::{MhUi, Ui}, + handler::MockIEventHandler, + lib::MockIMusicHoard, + listener::MockIEventListener, + ui::{IUi, Ui}, Error, Tui, }; @@ -178,21 +174,17 @@ mod tests { Terminal::new(backend).unwrap() } - pub fn ui(collection: Collection) -> MhUi { - let mut collection_manager = MockCollectionManager::new(); + pub fn ui(collection: Collection) -> Ui { + let mut music_hoard = MockIMusicHoard::new(); - collection_manager - .expect_rescan_library() - .returning(|| Ok(())); - collection_manager - .expect_get_collection() - .return_const(collection); + music_hoard.expect_rescan_library().returning(|| Ok(())); + music_hoard.expect_get_collection().return_const(collection); - MhUi::new(collection_manager).unwrap() + Ui::new(music_hoard).unwrap() } - fn listener() -> MockEventListener { - let mut listener = MockEventListener::new(); + fn listener() -> MockIEventListener { + let mut listener = MockIEventListener::new(); listener.expect_spawn().return_once(|| { thread::spawn(|| { thread::park(); @@ -202,11 +194,11 @@ mod tests { listener } - fn handler() -> MockEventHandler> { - let mut handler = MockEventHandler::new(); + fn handler() -> MockIEventHandler> { + let mut handler = MockIEventHandler::new(); handler .expect_handle_next_event() - .return_once(|ui: &mut MhUi| { + .return_once(|ui: &mut Ui| { ui.quit(); Ok(()) }); @@ -232,7 +224,7 @@ mod tests { let listener = listener(); - let mut handler = MockEventHandler::new(); + let mut handler = MockIEventHandler::new(); handler .expect_handle_next_event() .return_once(|_| Err(EventError::Recv)); @@ -254,10 +246,10 @@ mod tests { let listener_handle: thread::JoinHandle = thread::spawn(|| error); while !listener_handle.is_finished() {} - let mut listener = MockEventListener::new(); + let mut listener = MockIEventListener::new(); listener.expect_spawn().return_once(|| listener_handle); - let mut handler = MockEventHandler::new(); + let mut handler = MockIEventHandler::new(); handler .expect_handle_next_event() .return_once(|_| Err(EventError::Recv)); @@ -277,10 +269,10 @@ mod tests { let listener_handle: thread::JoinHandle = thread::spawn(|| panic!()); while !listener_handle.is_finished() {} - let mut listener = MockEventListener::new(); + let mut listener = MockIEventListener::new(); listener.expect_spawn().return_once(|| listener_handle); - let mut handler = MockEventHandler::new(); + let mut handler = MockIEventHandler::new(); handler .expect_handle_next_event() .return_once(|_| Err(EventError::Recv)); @@ -292,12 +284,12 @@ mod tests { #[test] fn errors() { - let collection_err: Error = collection::Error::DatabaseError(String::from("")).into(); + let lib_err: Error = musichoard::Error::DatabaseError(String::from("")).into(); let io_err: Error = io::Error::new(io::ErrorKind::Interrupted, "error").into(); let event_err: Error = EventError::Recv.into(); let listener_err = Error::ListenerPanic; - assert!(!format!("{:?}", collection_err).is_empty()); + assert!(!format!("{:?}", lib_err).is_empty()); assert!(!format!("{:?}", io_err).is_empty()); assert!(!format!("{:?}", event_err).is_empty()); assert!(!format!("{:?}", listener_err).is_empty()); diff --git a/src/tui/ui.rs b/src/tui/ui.rs index cb13a98..1fdc1cf 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -1,7 +1,4 @@ -use musichoard::{ - collection::{Collection, CollectionManager}, - Album, Artist, Track, TrackFormat, -}; +use musichoard::{Album, Artist, Collection, Track, TrackFormat}; use ratatui::{ backend::Backend, layout::{Alignment, Rect}, @@ -10,7 +7,20 @@ use ratatui::{ Frame, }; -use super::Error; +use super::{lib::IMusicHoard, Error}; + +pub trait IUi { + fn is_running(&self) -> bool; + fn quit(&mut self); + + fn increment_category(&mut self); + fn decrement_category(&mut self); + + fn increment_selection(&mut self); + fn decrement_selection(&mut self); + + fn render(&mut self, frame: &mut Frame<'_, B>); +} struct TrackSelection { state: ListState, @@ -239,8 +249,8 @@ impl Selection { } } -pub struct MhUi { - collection_manager: CM, +pub struct Ui { + music_hoard: MH, selection: Selection, running: bool, } @@ -428,12 +438,12 @@ impl<'a, 'b> TrackState<'a, 'b> { } } -impl MhUi { - pub fn new(mut collection_manager: CM) -> Result { - collection_manager.rescan_library()?; - let selection = Selection::new(Some(collection_manager.get_collection())); - Ok(MhUi { - collection_manager, +impl Ui { + pub fn new(mut music_hoard: MH) -> Result { + music_hoard.rescan_library()?; + let selection = Selection::new(Some(music_hoard.get_collection())); + Ok(Ui { + music_hoard, selection, running: true, }) @@ -512,20 +522,7 @@ impl MhUi { } } -pub trait Ui { - fn is_running(&self) -> bool; - fn quit(&mut self); - - fn increment_category(&mut self); - fn decrement_category(&mut self); - - fn increment_selection(&mut self); - fn decrement_selection(&mut self); - - fn render(&mut self, frame: &mut Frame<'_, B>); -} - -impl Ui for MhUi { +impl IUi for Ui { fn is_running(&self) -> bool { self.running } @@ -544,19 +541,19 @@ impl Ui for MhUi { fn increment_selection(&mut self) { self.selection - .increment_selection(self.collection_manager.get_collection()); + .increment_selection(self.music_hoard.get_collection()); } fn decrement_selection(&mut self) { self.selection - .decrement_selection(self.collection_manager.get_collection()); + .decrement_selection(self.music_hoard.get_collection()); } fn render(&mut self, frame: &mut Frame<'_, B>) { let active = self.selection.active; let areas = FrameArea::new(frame.size()); - let artists = self.collection_manager.get_collection(); + let artists = self.music_hoard.get_collection(); let artist_selection = &mut self.selection.artist; let artist_state = ArtistState::new( active == Category::Artist, @@ -600,7 +597,8 @@ impl Ui for MhUi { #[cfg(test)] mod tests { - use crate::tests::{MockCollectionManager, COLLECTION}; + use crate::tests::COLLECTION; + use crate::tui::lib::MockIMusicHoard; use crate::tui::tests::{terminal, ui}; use super::*; @@ -759,17 +757,17 @@ mod tests { #[test] fn ui_running() { - let mut collection_manager = MockCollectionManager::new(); + let mut music_hoard = MockIMusicHoard::new(); - collection_manager + music_hoard .expect_rescan_library() .times(1) .return_once(|| Ok(())); - collection_manager + music_hoard .expect_get_collection() .return_const(COLLECTION.to_owned()); - let mut ui = MhUi::new(collection_manager).unwrap(); + let mut ui = Ui::new(music_hoard).unwrap(); assert!(ui.is_running()); ui.quit(); @@ -778,17 +776,17 @@ mod tests { #[test] fn ui_modifiers() { - let mut collection_manager = MockCollectionManager::new(); + let mut music_hoard = MockIMusicHoard::new(); - collection_manager + music_hoard .expect_rescan_library() .times(1) .return_once(|| Ok(())); - collection_manager + music_hoard .expect_get_collection() .return_const(COLLECTION.to_owned()); - let mut ui = MhUi::new(collection_manager).unwrap(); + let mut ui = Ui::new(music_hoard).unwrap(); assert!(ui.is_running()); assert_eq!(ui.selection.active, Category::Artist); @@ -877,19 +875,17 @@ mod tests { #[test] fn app_no_tracks() { - let mut collection_manager = MockCollectionManager::new(); + let mut music_hoard = MockIMusicHoard::new(); let mut collection = COLLECTION.to_owned(); collection[0].albums[0].tracks = vec![]; - collection_manager + music_hoard .expect_rescan_library() .times(1) .return_once(|| Ok(())); - collection_manager - .expect_get_collection() - .return_const(collection); + music_hoard.expect_get_collection().return_const(collection); - let mut app = MhUi::new(collection_manager).unwrap(); + let mut app = Ui::new(music_hoard).unwrap(); assert!(app.is_running()); assert_eq!(app.selection.active, Category::Artist); @@ -915,19 +911,17 @@ mod tests { #[test] fn app_no_albums() { - let mut collection_manager = MockCollectionManager::new(); + let mut music_hoard = MockIMusicHoard::new(); let mut collection = COLLECTION.to_owned(); collection[0].albums = vec![]; - collection_manager + music_hoard .expect_rescan_library() .times(1) .return_once(|| Ok(())); - collection_manager - .expect_get_collection() - .return_const(collection); + music_hoard.expect_get_collection().return_const(collection); - let mut app = MhUi::new(collection_manager).unwrap(); + let mut app = Ui::new(music_hoard).unwrap(); assert!(app.is_running()); assert_eq!(app.selection.active, Category::Artist); @@ -966,18 +960,16 @@ mod tests { #[test] fn app_no_artists() { - let mut collection_manager = MockCollectionManager::new(); + let mut music_hoard = MockIMusicHoard::new(); let collection = vec![]; - collection_manager + music_hoard .expect_rescan_library() .times(1) .return_once(|| Ok(())); - collection_manager - .expect_get_collection() - .return_const(collection); + music_hoard.expect_get_collection().return_const(collection); - let mut app = MhUi::new(collection_manager).unwrap(); + let mut app = Ui::new(music_hoard).unwrap(); assert!(app.is_running()); assert_eq!(app.selection.active, Category::Artist); diff --git a/tests/database/json.rs b/tests/database/json.rs index 55ea7a6..07fbdf6 100644 --- a/tests/database/json.rs +++ b/tests/database/json.rs @@ -3,7 +3,7 @@ use std::{fs, path::PathBuf}; use musichoard::{ database::{ json::{backend::JsonDatabaseFileBackend, JsonDatabase}, - Database, + IDatabase, }, Artist, }; diff --git a/tests/library/beets.rs b/tests/library/beets.rs index 3d9215c..eb8f5f6 100644 --- a/tests/library/beets.rs +++ b/tests/library/beets.rs @@ -9,7 +9,7 @@ use once_cell::sync::Lazy; use musichoard::{ library::{ beets::{executor::BeetsLibraryProcessExecutor, BeetsLibrary}, - Field, Library, Query, + Field, ILibrary, Query, }, Artist, };