Replace as many Box<dyn Trait> with generics as possible (#32)

Closes #31

Reviewed-on: https://git.wojciechkozlowski.eu/wojtek/musichoard/pulls/32
This commit is contained in:
Wojciech Kozlowski 2023-04-13 15:29:14 +02:00
parent 14a0567fa1
commit 3e6c95d188
11 changed files with 272 additions and 220 deletions

View File

@ -56,18 +56,15 @@ pub trait CollectionManager {
/// The collection manager. It is responsible for pulling information from both the library and the /// The collection manager. 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 MhCollectionManager { pub struct MhCollectionManager<LIB, DB> {
library: Box<dyn Library + Send + Sync>, library: LIB,
database: Box<dyn Database + Send + Sync>, database: DB,
collection: Collection, collection: Collection,
} }
impl MhCollectionManager { impl<LIB: Library, DB: Database> MhCollectionManager<LIB, DB> {
/// Create a new [`CollectionManager`] with the provided [`Library`] and [`Database`]. /// Create a new [`CollectionManager`] with the provided [`Library`] and [`Database`].
pub fn new( pub fn new(library: LIB, database: DB) -> Self {
library: Box<dyn Library + Send + Sync>,
database: Box<dyn Database + Send + Sync>,
) -> Self {
MhCollectionManager { MhCollectionManager {
library, library,
database, database,
@ -76,7 +73,7 @@ impl MhCollectionManager {
} }
} }
impl CollectionManager for MhCollectionManager { impl<LIB: Library, DB: Database> CollectionManager for MhCollectionManager<LIB, DB> {
fn rescan_library(&mut self) -> Result<(), Error> { fn rescan_library(&mut self) -> Result<(), Error> {
self.collection = self.library.list(&Query::default())?; self.collection = self.library.list(&Query::default())?;
Ok(()) Ok(())
@ -97,6 +94,7 @@ mod tests {
use mockall::predicate; use mockall::predicate;
use crate::{ use crate::{
collection::Collection,
database::{self, MockDatabase}, database::{self, MockDatabase},
library::{self, MockLibrary, Query}, library::{self, MockLibrary, Query},
tests::COLLECTION, tests::COLLECTION,
@ -125,10 +123,9 @@ mod tests {
.expect_write() .expect_write()
.with(predicate::eq(database_input)) .with(predicate::eq(database_input))
.times(1) .times(1)
.return_once(|_| database_result); .return_once(|_: &Collection| database_result);
let mut collection_manager = let mut collection_manager = MhCollectionManager::new(library, database);
MhCollectionManager::new(Box::new(library), Box::new(database));
collection_manager.rescan_library().unwrap(); collection_manager.rescan_library().unwrap();
assert_eq!(collection_manager.get_collection(), &*COLLECTION); assert_eq!(collection_manager.get_collection(), &*COLLECTION);
@ -147,8 +144,7 @@ mod tests {
.times(1) .times(1)
.return_once(|_| library_result); .return_once(|_| library_result);
let mut collection_manager = let mut collection_manager = MhCollectionManager::new(library, database);
MhCollectionManager::new(Box::new(library), Box::new(database));
let actual_err = collection_manager.rescan_library().unwrap_err(); let actual_err = collection_manager.rescan_library().unwrap_err();
let expected_err = Error::LibraryError( let expected_err = Error::LibraryError(
@ -169,10 +165,9 @@ mod tests {
database database
.expect_write() .expect_write()
.times(1) .times(1)
.return_once(|_| database_result); .return_once(|_: &Collection| database_result);
let mut collection_manager = let mut collection_manager = MhCollectionManager::new(library, database);
MhCollectionManager::new(Box::new(library), Box::new(database));
let actual_err = collection_manager.save_to_database().unwrap_err(); let actual_err = collection_manager.save_to_database().unwrap_err();
let expected_err = let expected_err =

View File

@ -3,7 +3,8 @@
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use crate::collection::Collection; use serde::de::DeserializeOwned;
use serde::Serialize;
#[cfg(test)] #[cfg(test)]
use mockall::automock; use mockall::automock;
@ -27,25 +28,25 @@ pub trait JsonDatabaseBackend {
} }
/// JSON database. /// JSON database.
pub struct JsonDatabase { pub struct JsonDatabase<JDB> {
backend: Box<dyn JsonDatabaseBackend + Send + Sync>, backend: JDB,
} }
impl JsonDatabase { impl<JDB: JsonDatabaseBackend> JsonDatabase<JDB> {
/// Create a new JSON database with the provided backend, e.g. [`JsonDatabaseFileBackend`]. /// Create a new JSON database with the provided backend, e.g. [`JsonDatabaseFileBackend`].
pub fn new(backend: Box<dyn JsonDatabaseBackend + Send + Sync>) -> Self { pub fn new(backend: JDB) -> Self {
JsonDatabase { backend } JsonDatabase { backend }
} }
} }
impl Database for JsonDatabase { impl<JDB: JsonDatabaseBackend> Database for JsonDatabase<JDB> {
fn read(&self, collection: &mut Collection) -> Result<(), Error> { fn read<D: DeserializeOwned>(&self, collection: &mut D) -> Result<(), Error> {
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 write(&mut self, collection: &Collection) -> Result<(), Error> { fn write<S: Serialize>(&mut self, collection: &S) -> Result<(), Error> {
let serialized = serde_json::to_string(&collection)?; let serialized = serde_json::to_string(&collection)?;
self.backend.write(&serialized)?; self.backend.write(&serialized)?;
Ok(()) Ok(())
@ -162,9 +163,7 @@ mod tests {
.times(1) .times(1)
.return_once(|_| Ok(())); .return_once(|_| Ok(()));
JsonDatabase::new(Box::new(backend)) JsonDatabase::new(backend).write(&write_data).unwrap();
.write(&write_data)
.unwrap();
} }
#[test] #[test]
@ -176,9 +175,7 @@ mod tests {
backend.expect_read().times(1).return_once(|| result); backend.expect_read().times(1).return_once(|| result);
let mut read_data: Vec<Artist> = vec![]; let mut read_data: Vec<Artist> = vec![];
JsonDatabase::new(Box::new(backend)) JsonDatabase::new(backend).read(&mut read_data).unwrap();
.read(&mut read_data)
.unwrap();
assert_eq!(read_data, expected); assert_eq!(read_data, expected);
} }
@ -197,7 +194,7 @@ mod tests {
.return_once(|_| Ok(())); .return_once(|_| Ok(()));
backend.expect_read().times(1).return_once(|| result); backend.expect_read().times(1).return_once(|| result);
let mut database = JsonDatabase::new(Box::new(backend)); let mut database = JsonDatabase::new(backend);
let write_data = COLLECTION.to_owned(); let write_data = COLLECTION.to_owned();
let mut read_data: Vec<Artist> = vec![]; let mut read_data: Vec<Artist> = vec![];

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::Collection;
pub mod json; pub mod json;
/// Error type for database calls. /// Error type for database calls.
@ -39,8 +39,8 @@ impl From<std::io::Error> for Error {
#[cfg_attr(test, automock)] #[cfg_attr(test, automock)]
pub trait Database { pub trait Database {
/// Read collection from the database. /// Read collection from the database.
fn read(&self, collection: &mut Collection) -> Result<(), Error>; fn read<D: DeserializeOwned + 'static>(&self, collection: &mut D) -> Result<(), Error>;
/// Write collection to the database. /// Write collection to the database.
fn write(&mut self, collection: &Collection) -> Result<(), Error>; fn write<S: Serialize + 'static>(&mut self, collection: &S) -> Result<(), Error>;
} }

View File

@ -16,6 +16,33 @@ use crate::{Album, AlbumId, Artist, ArtistId, Track, TrackFormat};
use super::{Error, Library, Query, QueryOption}; use super::{Error, Library, Query, QueryOption};
macro_rules! list_format_separator {
() => {
" -*^- "
};
}
const CMD_LIST: &str = "ls";
const LIST_FORMAT_SEPARATOR: &str = list_format_separator!();
const LIST_FORMAT_ARG: &str = concat!(
"--format=",
"$albumartist",
list_format_separator!(),
"$year",
list_format_separator!(),
"$album",
list_format_separator!(),
"$track",
list_format_separator!(),
"$title",
list_format_separator!(),
"$artist",
list_format_separator!(),
"$format"
);
const TRACK_FORMAT_FLAC: &str = "FLAC";
const TRACK_FORMAT_MP3: &str = "MP3";
trait QueryOptionArgBeets { trait QueryOptionArgBeets {
fn to_arg(&self, option_name: &str) -> Option<String>; fn to_arg(&self, option_name: &str) -> Option<String>;
} }
@ -94,29 +121,23 @@ pub trait BeetsLibraryExecutor {
} }
/// Beets library. /// Beets library.
pub struct BeetsLibrary { pub struct BeetsLibrary<BLE> {
executor: Box<dyn BeetsLibraryExecutor + Send + Sync>, executor: BLE,
} }
trait LibraryPrivate { trait LibraryPrivate {
const CMD_LIST: &'static str;
const LIST_FORMAT_SEPARATOR: &'static str;
const LIST_FORMAT_ARG: &'static str;
const TRACK_FORMAT_FLAC: &'static str;
const TRACK_FORMAT_MP3: &'static str;
fn list_cmd_and_args(query: &Query) -> Vec<String>; fn list_cmd_and_args(query: &Query) -> Vec<String>;
fn list_to_artists(list_output: Vec<String>) -> Result<Vec<Artist>, Error>; fn list_to_artists(list_output: Vec<String>) -> Result<Vec<Artist>, Error>;
} }
impl BeetsLibrary { impl<BLE: BeetsLibraryExecutor> BeetsLibrary<BLE> {
/// Create a new beets library with the provided executor, e.g. [`BeetsLibraryCommandExecutor`]. /// Create a new beets library with the provided executor, e.g. [`BeetsLibraryCommandExecutor`].
pub fn new(executor: Box<dyn BeetsLibraryExecutor + Send + Sync>) -> BeetsLibrary { pub fn new(executor: BLE) -> Self {
BeetsLibrary { executor } BeetsLibrary { executor }
} }
} }
impl Library for BeetsLibrary { impl<BLE: BeetsLibraryExecutor> Library for BeetsLibrary<BLE> {
fn list(&mut self, query: &Query) -> Result<Vec<Artist>, Error> { fn list(&mut self, query: &Query) -> Result<Vec<Artist>, Error> {
let cmd = Self::list_cmd_and_args(query); let cmd = Self::list_cmd_and_args(query);
let output = self.executor.exec(&cmd)?; let output = self.executor.exec(&cmd)?;
@ -124,37 +145,10 @@ impl Library for BeetsLibrary {
} }
} }
macro_rules! list_format_separator { impl<BLE: BeetsLibraryExecutor> LibraryPrivate for BeetsLibrary<BLE> {
() => {
" -*^- "
};
}
impl LibraryPrivate for BeetsLibrary {
const CMD_LIST: &'static str = "ls";
const LIST_FORMAT_SEPARATOR: &'static str = list_format_separator!();
const LIST_FORMAT_ARG: &'static str = concat!(
"--format=",
"$albumartist",
list_format_separator!(),
"$year",
list_format_separator!(),
"$album",
list_format_separator!(),
"$track",
list_format_separator!(),
"$title",
list_format_separator!(),
"$artist",
list_format_separator!(),
"$format"
);
const TRACK_FORMAT_FLAC: &'static str = "FLAC";
const TRACK_FORMAT_MP3: &'static str = "MP3";
fn list_cmd_and_args(query: &Query) -> Vec<String> { fn list_cmd_and_args(query: &Query) -> Vec<String> {
let mut cmd: Vec<String> = vec![String::from(Self::CMD_LIST)]; let mut cmd: Vec<String> = vec![String::from(CMD_LIST)];
cmd.push(Self::LIST_FORMAT_ARG.to_string()); cmd.push(LIST_FORMAT_ARG.to_string());
cmd.append(&mut query.to_args()); cmd.append(&mut query.to_args());
cmd cmd
} }
@ -168,7 +162,7 @@ impl LibraryPrivate for BeetsLibrary {
continue; continue;
} }
let split: Vec<&str> = line.split(Self::LIST_FORMAT_SEPARATOR).collect(); let split: Vec<&str> = line.split(LIST_FORMAT_SEPARATOR).collect();
if split.len() != 7 { if split.len() != 7 {
return Err(Error::InvalidData(line.to_string())); return Err(Error::InvalidData(line.to_string()));
} }
@ -193,8 +187,8 @@ impl LibraryPrivate for BeetsLibrary {
title: track_title, title: track_title,
artist: track_artist.split("; ").map(|s| s.to_owned()).collect(), artist: track_artist.split("; ").map(|s| s.to_owned()).collect(),
format: match track_format.as_ref() { format: match track_format.as_ref() {
Self::TRACK_FORMAT_FLAC => TrackFormat::Flac, TRACK_FORMAT_FLAC => TrackFormat::Flac,
Self::TRACK_FORMAT_MP3 => TrackFormat::Mp3, TRACK_FORMAT_MP3 => TrackFormat::Mp3,
_ => return Err(Error::InvalidData(track_format)), _ => return Err(Error::InvalidData(track_format)),
}, },
}; };
@ -309,14 +303,14 @@ mod tests {
let track_title = &track.title; let track_title = &track.title;
let track_artist = &track.artist.join("; "); let track_artist = &track.artist.join("; ");
let track_format = match track.format { let track_format = match track.format {
TrackFormat::Flac => BeetsLibrary::TRACK_FORMAT_FLAC, TrackFormat::Flac => TRACK_FORMAT_FLAC,
TrackFormat::Mp3 => BeetsLibrary::TRACK_FORMAT_MP3, TrackFormat::Mp3 => TRACK_FORMAT_MP3,
}; };
strings.push(format!( strings.push(format!(
"{album_artist}{0}{album_year}{0}{album_title}{0}\ "{album_artist}{0}{album_year}{0}{album_title}{0}\
{track_number}{0}{track_title}{0}{track_artist}{0}{track_format}", {track_number}{0}{track_title}{0}{track_artist}{0}{track_format}",
BeetsLibrary::LIST_FORMAT_SEPARATOR, LIST_FORMAT_SEPARATOR,
)); ));
} }
} }
@ -356,7 +350,7 @@ mod tests {
#[test] #[test]
fn test_list_empty() { fn test_list_empty() {
let arguments = vec!["ls".to_string(), BeetsLibrary::LIST_FORMAT_ARG.to_string()]; let arguments = vec!["ls".to_string(), LIST_FORMAT_ARG.to_string()];
let result = Ok(vec![]); let result = Ok(vec![]);
let mut executor = MockBeetsLibraryExecutor::new(); let mut executor = MockBeetsLibraryExecutor::new();
@ -366,7 +360,7 @@ mod tests {
.times(1) .times(1)
.return_once(|_| result); .return_once(|_| result);
let mut beets = BeetsLibrary::new(Box::new(executor)); let mut beets = BeetsLibrary::new(executor);
let output = beets.list(&Query::default()).unwrap(); let output = beets.list(&Query::default()).unwrap();
let expected: Vec<Artist> = vec![]; let expected: Vec<Artist> = vec![];
@ -375,7 +369,7 @@ mod tests {
#[test] #[test]
fn test_list_ordered() { fn test_list_ordered() {
let arguments = vec!["ls".to_string(), BeetsLibrary::LIST_FORMAT_ARG.to_string()]; let arguments = vec!["ls".to_string(), LIST_FORMAT_ARG.to_string()];
let expected = COLLECTION.to_owned(); let expected = COLLECTION.to_owned();
let result = Ok(artists_to_beets_string(&expected)); let result = Ok(artists_to_beets_string(&expected));
@ -386,7 +380,7 @@ mod tests {
.times(1) .times(1)
.return_once(|_| result); .return_once(|_| result);
let mut beets = BeetsLibrary::new(Box::new(executor)); let mut beets = BeetsLibrary::new(executor);
let output = beets.list(&Query::default()).unwrap(); let output = beets.list(&Query::default()).unwrap();
assert_eq!(output, expected); assert_eq!(output, expected);
@ -394,7 +388,7 @@ mod tests {
#[test] #[test]
fn test_list_unordered() { fn test_list_unordered() {
let arguments = vec!["ls".to_string(), BeetsLibrary::LIST_FORMAT_ARG.to_string()]; let arguments = vec!["ls".to_string(), LIST_FORMAT_ARG.to_string()];
let mut expected = COLLECTION.to_owned(); let mut expected = COLLECTION.to_owned();
let mut output = artists_to_beets_string(&expected); let mut output = artists_to_beets_string(&expected);
let last = output.len() - 1; let last = output.len() - 1;
@ -421,7 +415,7 @@ mod tests {
.times(1) .times(1)
.return_once(|_| result); .return_once(|_| result);
let mut beets = BeetsLibrary::new(Box::new(executor)); let mut beets = BeetsLibrary::new(executor);
let output = beets.list(&Query::default()).unwrap(); let output = beets.list(&Query::default()).unwrap();
assert_eq!(output, expected); assert_eq!(output, expected);
@ -429,7 +423,7 @@ mod tests {
#[test] #[test]
fn test_list_album_title_year_clash() { fn test_list_album_title_year_clash() {
let arguments = vec!["ls".to_string(), BeetsLibrary::LIST_FORMAT_ARG.to_string()]; let arguments = vec!["ls".to_string(), LIST_FORMAT_ARG.to_string()];
let mut expected = COLLECTION.to_owned(); let mut expected = COLLECTION.to_owned();
expected[0].albums[0].id.year = expected[1].albums[0].id.year; expected[0].albums[0].id.year = expected[1].albums[0].id.year;
expected[0].albums[0].id.title = expected[1].albums[0].id.title.clone(); expected[0].albums[0].id.title = expected[1].albums[0].id.title.clone();
@ -443,7 +437,7 @@ mod tests {
.times(1) .times(1)
.return_once(|_| result); .return_once(|_| result);
let mut beets = BeetsLibrary::new(Box::new(executor)); let mut beets = BeetsLibrary::new(executor);
let output = beets.list(&Query::default()).unwrap(); let output = beets.list(&Query::default()).unwrap();
assert_eq!(output, expected); assert_eq!(output, expected);
@ -458,7 +452,7 @@ mod tests {
let arguments = vec![ let arguments = vec![
"ls".to_string(), "ls".to_string(),
BeetsLibrary::LIST_FORMAT_ARG.to_string(), LIST_FORMAT_ARG.to_string(),
String::from("^album:some.album"), String::from("^album:some.album"),
String::from("track:5"), String::from("track:5"),
String::from("artist:some.artist"), String::from("artist:some.artist"),
@ -472,7 +466,7 @@ mod tests {
.times(1) .times(1)
.return_once(|_| result); .return_once(|_| result);
let mut beets = BeetsLibrary::new(Box::new(executor)); let mut beets = BeetsLibrary::new(executor);
let output = beets.list(&query).unwrap(); let output = beets.list(&query).unwrap();
let expected: Vec<Artist> = vec![]; let expected: Vec<Artist> = vec![];

View File

@ -13,7 +13,7 @@ use musichoard::{
mod tui; mod tui;
use tui::{ use tui::{
app::App, event::EventChannel, handler::TuiEventHandler, listener::TuiEventListener, ui::Ui, app::TuiApp, event::EventChannel, handler::TuiEventHandler, listener::TuiEventListener, ui::Ui,
Tui, Tui,
}; };
@ -41,15 +41,13 @@ fn main() {
// Create the application. // Create the application.
let opt = Opt::from_args(); let opt = Opt::from_args();
let beets = BeetsLibrary::new(Box::new( let beets = BeetsLibrary::new(
BeetsLibraryCommandExecutor::default().config(opt.beets_config_file_path.as_deref()), BeetsLibraryCommandExecutor::default().config(opt.beets_config_file_path.as_deref()),
)); );
let database = JsonDatabase::new(Box::new(JsonDatabaseFileBackend::new( let database = JsonDatabase::new(JsonDatabaseFileBackend::new(&opt.database_file_path));
&opt.database_file_path,
)));
let collection_manager = MhCollectionManager::new(Box::new(beets), Box::new(database)); let collection_manager = MhCollectionManager::new(beets, database);
// Initialize the terminal user interface. // Initialize the terminal user interface.
let backend = CrosstermBackend::new(io::stdout()); let backend = CrosstermBackend::new(io::stdout());
@ -61,7 +59,7 @@ fn main() {
let ui = Ui::new(); let ui = Ui::new();
let app = App::new(Box::new(collection_manager)).expect("failed to initialise app"); let app = TuiApp::new(collection_manager).expect("failed to initialise app");
// Run the TUI application. // Run the TUI application.
Tui::run(terminal, app, ui, handler, listener).expect("failed to run tui"); Tui::run(terminal, app, ui, handler, listener).expect("failed to run tui");

View File

@ -110,31 +110,74 @@ struct Selection {
artist: Option<ArtistSelection>, artist: Option<ArtistSelection>,
} }
pub struct App { pub trait App {
collection_manager: Box<dyn CollectionManager>, 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 get_active_category(&self) -> Category;
fn get_artist_ids(&self) -> Vec<&ArtistId>;
fn get_album_ids(&self) -> Vec<&AlbumId>;
fn get_track_ids(&self) -> Vec<&Track>;
fn selected_artist(&self) -> Option<usize>;
fn selected_album(&self) -> Option<usize>;
fn selected_track(&self) -> Option<usize>;
}
trait AppPrivate {
fn increment_artist_selection(&mut self);
fn decrement_artist_selection(&mut self);
fn increment_album_selection(&mut self);
fn decrement_album_selection(&mut self);
fn increment_track_selection(&mut self);
fn decrement_track_selection(&mut self);
fn get_artists(&self) -> &Vec<Artist>;
fn get_albums(&self) -> Option<&Vec<Album>>;
fn get_tracks(&self) -> Option<&Vec<Track>>;
}
pub struct TuiApp<CM> {
collection_manager: CM,
selection: Selection, selection: Selection,
running: bool, running: bool,
} }
impl App { impl<CM: CollectionManager> TuiApp<CM> {
pub fn new(mut collection_manager: Box<dyn CollectionManager>) -> Result<Self, Error> { pub fn new(mut collection_manager: CM) -> Result<Self, Error> {
collection_manager.rescan_library()?; collection_manager.rescan_library()?;
let selection = Selection { let selection = Selection {
active: Category::Artist, active: Category::Artist,
artist: ArtistSelection::initialise(collection_manager.get_collection()), artist: ArtistSelection::initialise(collection_manager.get_collection()),
}; };
Ok(App { Ok(TuiApp {
collection_manager, collection_manager,
selection, selection,
running: true, running: true,
}) })
} }
}
pub fn is_running(&self) -> bool { impl<CM: CollectionManager> App for TuiApp<CM> {
fn is_running(&self) -> bool {
self.running self.running
} }
pub fn increment_category(&mut self) { fn quit(&mut self) {
self.running = false;
}
fn increment_category(&mut self) {
self.selection.active = match self.selection.active { self.selection.active = match self.selection.active {
Category::Artist => Category::Album, Category::Artist => Category::Album,
Category::Album => Category::Track, Category::Album => Category::Track,
@ -142,7 +185,7 @@ impl App {
}; };
} }
pub fn decrement_category(&mut self) { fn decrement_category(&mut self) {
self.selection.active = match self.selection.active { self.selection.active = match self.selection.active {
Category::Artist => Category::Artist, Category::Artist => Category::Artist,
Category::Album => Category::Artist, Category::Album => Category::Artist,
@ -150,7 +193,7 @@ impl App {
}; };
} }
pub fn increment_selection(&mut self) { fn increment_selection(&mut self) {
match self.selection.active { match self.selection.active {
Category::Artist => self.increment_artist_selection(), Category::Artist => self.increment_artist_selection(),
Category::Album => self.increment_album_selection(), Category::Album => self.increment_album_selection(),
@ -158,7 +201,7 @@ impl App {
} }
} }
pub fn decrement_selection(&mut self) { fn decrement_selection(&mut self) {
match self.selection.active { match self.selection.active {
Category::Artist => self.decrement_artist_selection(), Category::Artist => self.decrement_artist_selection(),
Category::Album => self.decrement_album_selection(), Category::Album => self.decrement_album_selection(),
@ -166,6 +209,57 @@ impl App {
} }
} }
fn get_active_category(&self) -> Category {
self.selection.active
}
fn get_artist_ids(&self) -> Vec<&ArtistId> {
let artists = self.get_artists();
artists.iter().map(|a| &a.id).collect()
}
fn get_album_ids(&self) -> Vec<&AlbumId> {
if let Some(albums) = self.get_albums() {
albums.iter().map(|a| &a.id).collect()
} else {
vec![]
}
}
fn get_track_ids(&self) -> Vec<&Track> {
if let Some(tracks) = self.get_tracks() {
tracks.iter().collect()
} else {
vec![]
}
}
fn selected_artist(&self) -> Option<usize> {
self.selection.artist.as_ref().map(|s| s.index)
}
fn selected_album(&self) -> Option<usize> {
if let Some(ref artist_selection) = self.selection.artist {
artist_selection.album.as_ref().map(|s| s.index)
} else {
None
}
}
fn selected_track(&self) -> Option<usize> {
if let Some(ref artist_selection) = self.selection.artist {
if let Some(ref album_selection) = artist_selection.album {
album_selection.track.as_ref().map(|s| s.index)
} else {
None
}
} else {
None
}
}
}
impl<CM: CollectionManager> AppPrivate for TuiApp<CM> {
fn increment_artist_selection(&mut self) { fn increment_artist_selection(&mut self) {
if let Some(ref mut artist_selection) = self.selection.artist { if let Some(ref mut artist_selection) = self.selection.artist {
let artists = &self.collection_manager.get_collection(); let artists = &self.collection_manager.get_collection();
@ -226,10 +320,6 @@ impl App {
} }
} }
pub fn get_active_category(&self) -> Category {
self.selection.active
}
fn get_artists(&self) -> &Vec<Artist> { fn get_artists(&self) -> &Vec<Artist> {
self.collection_manager.get_collection() self.collection_manager.get_collection()
} }
@ -254,55 +344,6 @@ impl App {
None None
} }
} }
pub fn get_artist_ids(&self) -> Vec<&ArtistId> {
let artists = self.get_artists();
artists.iter().map(|a| &a.id).collect()
}
pub fn get_album_ids(&self) -> Vec<&AlbumId> {
if let Some(albums) = self.get_albums() {
albums.iter().map(|a| &a.id).collect()
} else {
vec![]
}
}
pub fn get_track_ids(&self) -> Vec<&Track> {
if let Some(tracks) = self.get_tracks() {
tracks.iter().collect()
} else {
vec![]
}
}
pub fn selected_artist(&self) -> Option<usize> {
self.selection.artist.as_ref().map(|s| s.index)
}
pub fn selected_album(&self) -> Option<usize> {
if let Some(ref artist_selection) = self.selection.artist {
artist_selection.album.as_ref().map(|s| s.index)
} else {
None
}
}
pub fn selected_track(&self) -> Option<usize> {
if let Some(ref artist_selection) = self.selection.artist {
if let Some(ref album_selection) = artist_selection.album {
album_selection.track.as_ref().map(|s| s.index)
} else {
None
}
} else {
None
}
}
pub fn quit(&mut self) {
self.running = false;
}
} }
#[cfg(test)] #[cfg(test)]
@ -516,7 +557,7 @@ mod tests {
.expect_get_collection() .expect_get_collection()
.return_const(COLLECTION.to_owned()); .return_const(COLLECTION.to_owned());
let mut app = App::new(Box::new(collection_manager)).unwrap(); let mut app = TuiApp::new(collection_manager).unwrap();
assert!(app.is_running()); assert!(app.is_running());
app.quit(); app.quit();
@ -535,7 +576,7 @@ mod tests {
.expect_get_collection() .expect_get_collection()
.return_const(COLLECTION.to_owned()); .return_const(COLLECTION.to_owned());
let mut app = App::new(Box::new(collection_manager)).unwrap(); let mut app = TuiApp::new(collection_manager).unwrap();
assert!(app.is_running()); assert!(app.is_running());
assert!(!app.get_artist_ids().is_empty()); assert!(!app.get_artist_ids().is_empty());
@ -640,7 +681,7 @@ mod tests {
.expect_get_collection() .expect_get_collection()
.return_const(collection); .return_const(collection);
let mut app = App::new(Box::new(collection_manager)).unwrap(); let mut app = TuiApp::new(collection_manager).unwrap();
assert!(app.is_running()); assert!(app.is_running());
assert!(!app.get_artist_ids().is_empty()); assert!(!app.get_artist_ids().is_empty());
@ -682,7 +723,7 @@ mod tests {
.expect_get_collection() .expect_get_collection()
.return_const(collection); .return_const(collection);
let mut app = App::new(Box::new(collection_manager)).unwrap(); let mut app = TuiApp::new(collection_manager).unwrap();
assert!(app.is_running()); assert!(app.is_running());
assert!(!app.get_artist_ids().is_empty()); assert!(!app.get_artist_ids().is_empty());
@ -736,7 +777,7 @@ mod tests {
.expect_get_collection() .expect_get_collection()
.return_const(collection); .return_const(collection);
let mut app = App::new(Box::new(collection_manager)).unwrap(); let mut app = TuiApp::new(collection_manager).unwrap();
assert!(app.is_running()); assert!(app.is_running());
assert!(app.get_artist_ids().is_empty()); assert!(app.get_artist_ids().is_empty());

View File

@ -9,12 +9,12 @@ use super::{
}; };
#[cfg_attr(test, automock)] #[cfg_attr(test, automock)]
pub trait EventHandler { pub trait EventHandler<APP> {
fn handle_next_event(&self, app: &mut App) -> Result<(), EventError>; fn handle_next_event(&self, app: &mut APP) -> Result<(), EventError>;
} }
trait EventHandlerPrivate { trait EventHandlerPrivate<APP> {
fn handle_key_event(app: &mut App, key_event: KeyEvent); fn handle_key_event(app: &mut APP, key_event: KeyEvent);
} }
pub struct TuiEventHandler { pub struct TuiEventHandler {
@ -28,8 +28,8 @@ impl TuiEventHandler {
} }
} }
impl EventHandler for TuiEventHandler { impl<APP: App> EventHandler<APP> for TuiEventHandler {
fn handle_next_event(&self, app: &mut App) -> Result<(), EventError> { fn handle_next_event(&self, app: &mut APP) -> Result<(), EventError> {
match self.events.recv()? { match self.events.recv()? {
Event::Key(key_event) => Self::handle_key_event(app, key_event), Event::Key(key_event) => Self::handle_key_event(app, key_event),
Event::Mouse(_) => {} Event::Mouse(_) => {}
@ -39,8 +39,8 @@ impl EventHandler for TuiEventHandler {
} }
} }
impl EventHandlerPrivate for TuiEventHandler { impl<APP: App> EventHandlerPrivate<APP> for TuiEventHandler {
fn handle_key_event(app: &mut App, key_event: KeyEvent) { fn handle_key_event(app: &mut APP, key_event: KeyEvent) {
match key_event.code { match key_event.code {
// Exit application on `ESC` or `q`. // Exit application on `ESC` or `q`.
KeyCode::Esc | KeyCode::Char('q') => { KeyCode::Esc | KeyCode::Char('q') => {

View File

@ -4,6 +4,7 @@ use musichoard::collection;
use ratatui::backend::Backend; use ratatui::backend::Backend;
use ratatui::Terminal; use ratatui::Terminal;
use std::io; use std::io;
use std::marker::PhantomData;
pub mod app; pub mod app;
pub mod event; pub mod event;
@ -43,11 +44,12 @@ impl From<EventError> for Error {
} }
} }
pub struct Tui<B: Backend> { pub struct Tui<B: Backend, APP> {
terminal: Terminal<B>, terminal: Terminal<B>,
_phantom: PhantomData<APP>,
} }
impl<B: Backend> Tui<B> { impl<B: Backend, APP: App> Tui<B, APP> {
fn init(&mut self) -> Result<(), Error> { fn init(&mut self) -> Result<(), Error> {
self.terminal.hide_cursor()?; self.terminal.hide_cursor()?;
self.terminal.clear()?; self.terminal.clear()?;
@ -64,7 +66,12 @@ impl<B: Backend> Tui<B> {
self.exit(); self.exit();
} }
fn main_loop(&mut self, mut app: App, ui: Ui, handler: impl EventHandler) -> Result<(), Error> { fn main_loop(
&mut self,
mut app: APP,
ui: Ui<APP>,
handler: impl EventHandler<APP>,
) -> Result<(), Error> {
while app.is_running() { while app.is_running() {
self.terminal.draw(|frame| ui.render(&app, frame))?; self.terminal.draw(|frame| ui.render(&app, frame))?;
handler.handle_next_event(&mut app)?; handler.handle_next_event(&mut app)?;
@ -75,12 +82,15 @@ impl<B: Backend> Tui<B> {
fn main( fn main(
term: Terminal<B>, term: Terminal<B>,
app: App, app: APP,
ui: Ui, ui: Ui<APP>,
handler: impl EventHandler, handler: impl EventHandler<APP>,
listener: impl EventListener, listener: impl EventListener,
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut tui = Tui { terminal: term }; let mut tui = Tui {
terminal: term,
_phantom: PhantomData,
};
tui.init()?; tui.init()?;
@ -132,9 +142,9 @@ impl<B: Backend> Tui<B> {
pub fn run( pub fn run(
term: Terminal<B>, term: Terminal<B>,
app: App, app: APP,
ui: Ui, ui: Ui<APP>,
handler: impl EventHandler, handler: impl EventHandler<APP>,
listener: impl EventListener, listener: impl EventListener,
) -> Result<(), Error> { ) -> Result<(), Error> {
Self::enable()?; Self::enable()?;
@ -165,8 +175,12 @@ mod tests {
use crate::tests::{MockCollectionManager, COLLECTION}; use crate::tests::{MockCollectionManager, COLLECTION};
use super::{ use super::{
app::App, event::EventError, handler::MockEventHandler, listener::MockEventListener, app::{App, TuiApp},
ui::Ui, Error, Tui, event::EventError,
handler::MockEventHandler,
listener::MockEventListener,
ui::Ui,
Error, Tui,
}; };
pub fn terminal() -> Terminal<TestBackend> { pub fn terminal() -> Terminal<TestBackend> {
@ -174,7 +188,7 @@ mod tests {
Terminal::new(backend).unwrap() Terminal::new(backend).unwrap()
} }
pub fn app(collection: Collection) -> App { pub fn app(collection: Collection) -> TuiApp<MockCollectionManager> {
let mut collection_manager = MockCollectionManager::new(); let mut collection_manager = MockCollectionManager::new();
collection_manager collection_manager
@ -184,7 +198,7 @@ mod tests {
.expect_get_collection() .expect_get_collection()
.return_const(collection); .return_const(collection);
App::new(Box::new(collection_manager)).unwrap() TuiApp::new(collection_manager).unwrap()
} }
fn listener() -> MockEventListener { fn listener() -> MockEventListener {
@ -198,12 +212,14 @@ mod tests {
listener listener
} }
fn handler() -> MockEventHandler { fn handler() -> MockEventHandler<TuiApp<MockCollectionManager>> {
let mut handler = MockEventHandler::new(); let mut handler = MockEventHandler::new();
handler.expect_handle_next_event().return_once(|app| { handler.expect_handle_next_event().return_once(
|app: &mut TuiApp<MockCollectionManager>| {
app.quit(); app.quit();
Ok(()) Ok(())
}); },
);
handler handler
} }

View File

@ -1,3 +1,5 @@
use std::marker::PhantomData;
use musichoard::TrackFormat; use musichoard::TrackFormat;
use ratatui::{ use ratatui::{
backend::Backend, backend::Backend,
@ -57,11 +59,15 @@ struct AppState<'a> {
tracks: TrackState<'a>, tracks: TrackState<'a>,
} }
pub struct Ui {} pub struct Ui<APP> {
_phantom: PhantomData<APP>,
}
impl Ui { impl<APP: App> Ui<APP> {
pub fn new() -> Self { pub fn new() -> Self {
Ui {} Ui {
_phantom: PhantomData,
}
} }
fn construct_areas(frame: Rect) -> FrameAreas { fn construct_areas(frame: Rect) -> FrameAreas {
@ -121,7 +127,7 @@ impl Ui {
} }
} }
fn construct_artist_list(app: &App) -> ArtistState { fn construct_artist_list(app: &APP) -> ArtistState {
let artists = app.get_artist_ids(); let artists = app.get_artist_ids();
let list = List::new( let list = List::new(
artists artists
@ -143,7 +149,7 @@ impl Ui {
} }
} }
fn construct_album_list(app: &App) -> AlbumState { fn construct_album_list(app: &APP) -> AlbumState {
let albums = app.get_album_ids(); let albums = app.get_album_ids();
let list = List::new( let list = List::new(
albums albums
@ -176,7 +182,7 @@ impl Ui {
} }
} }
fn construct_track_list(app: &App) -> TrackState { fn construct_track_list(app: &APP) -> TrackState {
let tracks = app.get_track_ids(); let tracks = app.get_track_ids();
let list = List::new( let list = List::new(
tracks tracks
@ -220,7 +226,7 @@ impl Ui {
} }
} }
fn construct_app_state(app: &App) -> AppState { fn construct_app_state(app: &APP) -> AppState {
AppState { AppState {
artists: Self::construct_artist_list(app), artists: Self::construct_artist_list(app),
albums: Self::construct_album_list(app), albums: Self::construct_album_list(app),
@ -312,7 +318,7 @@ impl Ui {
Self::render_info_widget("Track info", state.info, state.active, area.info, frame); Self::render_info_widget("Track info", state.info, state.active, area.info, frame);
} }
pub fn render<B: Backend>(&self, app: &App, frame: &mut Frame<'_, B>) { pub fn render<B: Backend>(&self, app: &APP, frame: &mut Frame<'_, B>) {
let areas = Self::construct_areas(frame.size()); let areas = Self::construct_areas(frame.size());
let app_state = Self::construct_app_state(app); let app_state = Self::construct_app_state(app);
@ -328,7 +334,10 @@ mod tests {
use crate::{ use crate::{
tests::COLLECTION, tests::COLLECTION,
tui::tests::{app, terminal}, tui::{
app::App,
tests::{app, terminal},
},
}; };
use super::Ui; use super::Ui;

View File

@ -20,7 +20,7 @@ fn write() {
let file = NamedTempFile::new().unwrap(); let file = NamedTempFile::new().unwrap();
let backend = JsonDatabaseFileBackend::new(file.path()); let backend = JsonDatabaseFileBackend::new(file.path());
let mut database = JsonDatabase::new(Box::new(backend)); let mut database = JsonDatabase::new(backend);
let write_data = COLLECTION.to_owned(); let write_data = COLLECTION.to_owned();
database.write(&write_data).unwrap(); database.write(&write_data).unwrap();
@ -34,7 +34,7 @@ fn write() {
#[test] #[test]
fn read() { fn read() {
let backend = JsonDatabaseFileBackend::new(&*DATABASE_TEST_FILE); let backend = JsonDatabaseFileBackend::new(&*DATABASE_TEST_FILE);
let database = JsonDatabase::new(Box::new(backend)); let database = JsonDatabase::new(backend);
let mut read_data: Vec<Artist> = vec![]; let mut read_data: Vec<Artist> = vec![];
database.read(&mut read_data).unwrap(); database.read(&mut read_data).unwrap();
@ -48,7 +48,7 @@ fn reverse() {
let file = NamedTempFile::new().unwrap(); let file = NamedTempFile::new().unwrap();
let backend = JsonDatabaseFileBackend::new(file.path()); let backend = JsonDatabaseFileBackend::new(file.path());
let mut database = JsonDatabase::new(Box::new(backend)); let mut database = JsonDatabase::new(backend);
let write_data = COLLECTION.to_owned(); let write_data = COLLECTION.to_owned();
database.write(&write_data).unwrap(); database.write(&write_data).unwrap();

View File

@ -15,18 +15,20 @@ use musichoard::{
use crate::COLLECTION; use crate::COLLECTION;
static BEETS_EMPTY_CONFIG: Lazy<Arc<Mutex<BeetsLibrary>>> = Lazy::new(|| { static BEETS_EMPTY_CONFIG: Lazy<Arc<Mutex<BeetsLibrary<BeetsLibraryCommandExecutor>>>> =
Arc::new(Mutex::new(BeetsLibrary::new(Box::new( Lazy::new(|| {
Arc::new(Mutex::new(BeetsLibrary::new(
BeetsLibraryCommandExecutor::default(), BeetsLibraryCommandExecutor::default(),
)))) )))
}); });
static BEETS_TEST_CONFIG: Lazy<Arc<Mutex<BeetsLibrary>>> = Lazy::new(|| { static BEETS_TEST_CONFIG: Lazy<Arc<Mutex<BeetsLibrary<BeetsLibraryCommandExecutor>>>> =
Arc::new(Mutex::new(BeetsLibrary::new(Box::new( Lazy::new(|| {
Arc::new(Mutex::new(BeetsLibrary::new(
BeetsLibraryCommandExecutor::default().config(Some( BeetsLibraryCommandExecutor::default().config(Some(
&fs::canonicalize("./tests/files/library/config.yml").unwrap(), &fs::canonicalize("./tests/files/library/config.yml").unwrap(),
)), )),
)))) )))
}); });
#[test] #[test]