Integrate changes with binary

This commit is contained in:
Wojciech Kozlowski 2023-05-19 21:22:02 +02:00
parent 6a90b6cf78
commit e2dd53845f
10 changed files with 167 additions and 45 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
/target /target
/codecov /codecov
database.json

View File

@ -6,13 +6,19 @@ use serde::Serialize;
#[cfg(test)] #[cfg(test)]
use mockall::automock; use mockall::automock;
use super::{Error, IDatabase}; use super::{IDatabase, ReadError, WriteError};
pub mod backend; pub mod backend;
impl From<serde_json::Error> for Error { impl From<serde_json::Error> for ReadError {
fn from(err: serde_json::Error) -> Error { fn from(err: serde_json::Error) -> ReadError {
Error::SerDeError(err.to_string()) ReadError::SerDeError(err.to_string())
}
}
impl From<serde_json::Error> for WriteError {
fn from(err: serde_json::Error) -> WriteError {
WriteError::SerDeError(err.to_string())
} }
} }
@ -40,13 +46,13 @@ impl<JDB: IJsonDatabaseBackend> JsonDatabase<JDB> {
} }
impl<JDB: IJsonDatabaseBackend> IDatabase for JsonDatabase<JDB> { impl<JDB: IJsonDatabaseBackend> IDatabase for JsonDatabase<JDB> {
fn read<D: DeserializeOwned>(&self, collection: &mut D) -> Result<(), Error> { fn read<D: DeserializeOwned>(&self, collection: &mut D) -> Result<(), ReadError> {
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<S: Serialize>(&mut self, collection: &S) -> Result<(), Error> { fn write<S: Serialize>(&mut self, collection: &S) -> Result<(), WriteError> {
let serialized = serde_json::to_string(&collection)?; let serialized = serde_json::to_string(&collection)?;
self.backend.write(&serialized)?; self.backend.write(&serialized)?;
Ok(()) Ok(())
@ -191,7 +197,7 @@ mod tests {
let serde_err = serde_json::to_string(&object); let serde_err = serde_json::to_string(&object);
assert!(serde_err.is_err()); assert!(serde_err.is_err());
let serde_err: Error = serde_err.unwrap_err().into(); let serde_err: WriteError = serde_err.unwrap_err().into();
assert!(!serde_err.to_string().is_empty()); assert!(!serde_err.to_string().is_empty());
assert!(!format!("{:?}", serde_err).is_empty()); assert!(!format!("{:?}", serde_err).is_empty());
} }

View File

@ -14,35 +14,61 @@ pub mod json;
#[cfg_attr(test, automock)] #[cfg_attr(test, automock)]
pub trait IDatabase { pub trait IDatabase {
/// Read collection from the database. /// Read collection from the database.
fn read<D: DeserializeOwned + 'static>(&self, collection: &mut D) -> Result<(), Error>; fn read<D: DeserializeOwned + 'static>(&self, collection: &mut D) -> Result<(), ReadError>;
/// Write collection to the database. /// Write collection to the database.
fn write<S: Serialize + 'static>(&mut self, collection: &S) -> Result<(), Error>; fn write<S: Serialize + 'static>(&mut self, collection: &S) -> Result<(), WriteError>;
} }
/// Error type for database calls. /// Error type for database calls.
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum ReadError {
/// The database experienced an I/O error. /// The database experienced an I/O read error.
IoError(String), IoError(String),
/// The database experienced a (de)serialisation error. /// The database experienced a deserialisation error.
SerDeError(String), SerDeError(String),
} }
impl fmt::Display for Error { impl fmt::Display for ReadError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self { match *self {
Self::IoError(ref s) => write!(f, "the database experienced an I/O error: {s}"), Self::IoError(ref s) => write!(f, "the database experienced an I/O read error: {s}"),
Self::SerDeError(ref s) => { Self::SerDeError(ref s) => {
write!(f, "the database experienced a (de)serialisation error: {s}") write!(f, "the database experienced a deserialisation error: {s}")
} }
} }
} }
} }
impl From<std::io::Error> for Error { impl From<std::io::Error> for ReadError {
fn from(err: std::io::Error) -> Error { fn from(err: std::io::Error) -> ReadError {
Error::IoError(err.to_string()) ReadError::IoError(err.to_string())
}
}
/// Error type for database calls.
#[derive(Debug)]
pub enum WriteError {
/// The database experienced an I/O write error.
IoError(String),
/// The database experienced a serialisation error.
SerDeError(String),
}
impl fmt::Display for WriteError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Self::IoError(ref s) => write!(f, "the database experienced an I/O write error: {s}"),
Self::SerDeError(ref s) => {
write!(f, "the database experienced a serialisation error: {s}")
}
}
}
}
impl From<std::io::Error> for WriteError {
fn from(err: std::io::Error) -> WriteError {
WriteError::IoError(err.to_string())
} }
} }
@ -50,11 +76,15 @@ impl From<std::io::Error> for Error {
mod tests { mod tests {
use std::io; use std::io;
use super::Error; use super::{ReadError, WriteError};
#[test] #[test]
fn errors() { fn errors() {
let io_err: Error = io::Error::new(io::ErrorKind::Interrupted, "error").into(); let io_err: ReadError = io::Error::new(io::ErrorKind::Interrupted, "error").into();
assert!(!io_err.to_string().is_empty());
assert!(!format!("{:?}", io_err).is_empty());
let io_err: WriteError = io::Error::new(io::ErrorKind::Interrupted, "error").into();
assert!(!io_err.to_string().is_empty()); assert!(!io_err.to_string().is_empty());
assert!(!format!("{:?}", io_err).is_empty()); assert!(!format!("{:?}", io_err).is_empty());
} }

View File

@ -7,7 +7,8 @@ use std::{
cmp::Ordering, cmp::Ordering,
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
fmt, fmt,
iter::Peekable, mem, iter::Peekable,
mem,
}; };
use database::IDatabase; use database::IDatabase;
@ -212,8 +213,14 @@ impl From<library::Error> for Error {
} }
} }
impl From<database::Error> for Error { impl From<database::ReadError> for Error {
fn from(err: database::Error) -> Error { fn from(err: database::ReadError) -> Error {
Error::DatabaseError(err.to_string())
}
}
impl From<database::WriteError> for Error {
fn from(err: database::WriteError) -> Error {
Error::DatabaseError(err.to_string()) Error::DatabaseError(err.to_string())
} }
} }
@ -521,7 +528,7 @@ mod tests {
let library = MockILibrary::new(); let library = MockILibrary::new();
let mut database = MockIDatabase::new(); let mut database = MockIDatabase::new();
let database_result = Err(database::Error::IoError(String::from("I/O error"))); let database_result = Err(database::WriteError::IoError(String::from("I/O error")));
database database
.expect_write() .expect_write()
@ -531,8 +538,9 @@ mod tests {
let mut music_hoard = MusicHoard::new(library, database); let mut music_hoard = MusicHoard::new(library, database);
let actual_err = music_hoard.save_to_database().unwrap_err(); let actual_err = music_hoard.save_to_database().unwrap_err();
let expected_err = let expected_err = Error::DatabaseError(
Error::DatabaseError(database::Error::IoError(String::from("I/O error")).to_string()); database::WriteError::IoError(String::from("I/O error")).to_string(),
);
assert_eq!(actual_err, expected_err); assert_eq!(actual_err, expected_err);
assert_eq!(actual_err.to_string(), expected_err.to_string()); assert_eq!(actual_err.to_string(), expected_err.to_string());

View File

@ -1,6 +1,8 @@
use std::fs::OpenOptions;
use std::path::PathBuf; use std::path::PathBuf;
use std::{ffi::OsString, io}; use std::{ffi::OsString, io};
use musichoard::Collection;
use ratatui::{backend::CrosstermBackend, Terminal}; use ratatui::{backend::CrosstermBackend, Terminal};
use structopt::StructOpt; use structopt::StructOpt;
@ -57,9 +59,28 @@ fn with<LIB: ILibrary, DB: IDatabase>(lib: LIB, db: DB) {
} }
fn main() { fn main() {
// Create the application.
let opt = Opt::from_args(); let opt = Opt::from_args();
// Create an empty database file if it does not exist.
match OpenOptions::new()
.write(true)
.create_new(true)
.open(&opt.database_file_path)
{
Ok(f) => {
drop(f);
JsonDatabase::new(JsonDatabaseFileBackend::new(&opt.database_file_path))
.write::<Collection>(&vec![])
.expect("failed to create empty database");
}
Err(e) => match e.kind() {
io::ErrorKind::AlreadyExists => {}
_ => panic!("failed to access database file"),
},
}
// Create the application.
let db_exec = JsonDatabaseFileBackend::new(&opt.database_file_path);
if let Some(uri) = opt.beets_ssh_uri { if let Some(uri) = opt.beets_ssh_uri {
let uri = uri.into_string().expect("invalid SSH URI"); let uri = uri.into_string().expect("invalid SSH URI");
let beets_config_file_path = opt let beets_config_file_path = opt
@ -70,11 +91,9 @@ fn main() {
let lib_exec = BeetsLibrarySshExecutor::new(uri) let lib_exec = BeetsLibrarySshExecutor::new(uri)
.expect("failed to initialise beets") .expect("failed to initialise beets")
.config(beets_config_file_path); .config(beets_config_file_path);
let db_exec = JsonDatabaseFileBackend::new(&opt.database_file_path);
with(BeetsLibrary::new(lib_exec), JsonDatabase::new(db_exec)); with(BeetsLibrary::new(lib_exec), JsonDatabase::new(db_exec));
} else { } else {
let lib_exec = BeetsLibraryProcessExecutor::default().config(opt.beets_config_file_path); let lib_exec = BeetsLibraryProcessExecutor::default().config(opt.beets_config_file_path);
let db_exec = JsonDatabaseFileBackend::new(&opt.database_file_path);
with(BeetsLibrary::new(lib_exec), JsonDatabase::new(db_exec)); with(BeetsLibrary::new(lib_exec), JsonDatabase::new(db_exec));
} }
} }

View File

@ -2,11 +2,14 @@ use crossterm::event::{KeyEvent, MouseEvent};
use std::fmt; use std::fmt;
use std::sync::mpsc; use std::sync::mpsc;
use super::ui::UiError;
#[derive(Debug)] #[derive(Debug)]
pub enum EventError { pub enum EventError {
Send(Event), Send(Event),
Recv, Recv,
Io(std::io::Error), Io(std::io::Error),
Ui(String),
} }
impl fmt::Display for EventError { impl fmt::Display for EventError {
@ -17,6 +20,9 @@ impl fmt::Display for EventError {
Self::Io(ref e) => { Self::Io(ref e) => {
write!(f, "an I/O error was triggered during event handling: {e}") write!(f, "an I/O error was triggered during event handling: {e}")
} }
Self::Ui(ref s) => {
write!(f, "the UI returned an error during event handling: {s}")
}
} }
} }
} }
@ -33,6 +39,12 @@ impl From<mpsc::RecvError> for EventError {
} }
} }
impl From<UiError> for EventError {
fn from(err: UiError) -> EventError {
EventError::Ui(err.to_string())
}
}
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub enum Event { pub enum Event {
Key(KeyEvent), Key(KeyEvent),

View File

@ -14,7 +14,8 @@ pub trait IEventHandler<UI> {
} }
trait IEventHandlerPrivate<UI> { trait IEventHandlerPrivate<UI> {
fn handle_key_event(ui: &mut UI, key_event: KeyEvent); fn handle_key_event(ui: &mut UI, key_event: KeyEvent) -> Result<(), EventError>;
fn quit(ui: &mut UI) -> Result<(), EventError>;
} }
pub struct EventHandler { pub struct EventHandler {
@ -31,7 +32,7 @@ impl EventHandler {
impl<UI: IUi> IEventHandler<UI> for EventHandler { impl<UI: IUi> IEventHandler<UI> for EventHandler {
fn handle_next_event(&self, ui: &mut UI) -> Result<(), EventError> { fn handle_next_event(&self, ui: &mut UI) -> Result<(), EventError> {
match self.events.recv()? { match self.events.recv()? {
Event::Key(key_event) => Self::handle_key_event(ui, key_event), Event::Key(key_event) => Self::handle_key_event(ui, key_event)?,
Event::Mouse(_) => {} Event::Mouse(_) => {}
Event::Resize(_, _) => {} Event::Resize(_, _) => {}
}; };
@ -40,16 +41,16 @@ impl<UI: IUi> IEventHandler<UI> for EventHandler {
} }
impl<UI: IUi> IEventHandlerPrivate<UI> for EventHandler { impl<UI: IUi> IEventHandlerPrivate<UI> for EventHandler {
fn handle_key_event(ui: &mut UI, key_event: KeyEvent) { fn handle_key_event(ui: &mut UI, key_event: KeyEvent) -> Result<(), EventError> {
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') => {
ui.quit(); Self::quit(ui)?;
} }
// Exit application on `Ctrl-C`. // Exit application on `Ctrl-C`.
KeyCode::Char('c') | KeyCode::Char('C') => { KeyCode::Char('c') | KeyCode::Char('C') => {
if key_event.modifiers == KeyModifiers::CONTROL { if key_event.modifiers == KeyModifiers::CONTROL {
ui.quit(); Self::quit(ui)?;
} }
} }
// Category change. // Category change.
@ -69,6 +70,14 @@ impl<UI: IUi> IEventHandlerPrivate<UI> for EventHandler {
// Other keys. // Other keys.
_ => {} _ => {}
} }
Ok(())
}
fn quit(ui: &mut UI) -> Result<(), EventError> {
ui.quit();
ui.save()?;
Ok(())
} }
} }
// GRCOV_EXCL_STOP // GRCOV_EXCL_STOP

View File

@ -3,24 +3,26 @@ use musichoard::{database::IDatabase, library::ILibrary, Collection, MusicHoard}
#[cfg(test)] #[cfg(test)]
use mockall::automock; use mockall::automock;
use super::Error;
#[cfg_attr(test, automock)] #[cfg_attr(test, automock)]
pub trait IMusicHoard { pub trait IMusicHoard {
fn rescan_library(&mut self) -> Result<(), Error>; fn rescan_library(&mut self) -> Result<(), musichoard::Error>;
fn load_from_database(&mut self) -> Result<(), musichoard::Error>;
fn save_to_database(&mut self) -> Result<(), musichoard::Error>;
fn get_collection(&self) -> &Collection; fn get_collection(&self) -> &Collection;
} }
impl From<musichoard::Error> for Error {
fn from(err: musichoard::Error) -> Error {
Error::Lib(err.to_string())
}
}
// GRCOV_EXCL_START // GRCOV_EXCL_START
impl<LIB: ILibrary, DB: IDatabase> IMusicHoard for MusicHoard<LIB, DB> { impl<LIB: ILibrary, DB: IDatabase> IMusicHoard for MusicHoard<LIB, DB> {
fn rescan_library(&mut self) -> Result<(), Error> { fn rescan_library(&mut self) -> Result<(), musichoard::Error> {
Ok(MusicHoard::rescan_library(self)?) MusicHoard::rescan_library(self)
}
fn load_from_database(&mut self) -> Result<(), musichoard::Error> {
MusicHoard::load_from_database(self)
}
fn save_to_database(&mut self) -> Result<(), musichoard::Error> {
MusicHoard::save_to_database(self)
} }
fn get_collection(&self) -> &Collection { fn get_collection(&self) -> &Collection {

View File

@ -25,6 +25,12 @@ pub enum Error {
ListenerPanic, ListenerPanic,
} }
impl From<musichoard::Error> for Error {
fn from(err: musichoard::Error) -> Error {
Error::Lib(err.to_string())
}
}
impl From<io::Error> for Error { impl From<io::Error> for Error {
fn from(err: io::Error) -> Error { fn from(err: io::Error) -> Error {
Error::Io(err.to_string()) Error::Io(err.to_string())

View File

@ -1,3 +1,5 @@
use std::fmt;
use musichoard::{Album, Artist, Collection, Format, Track}; use musichoard::{Album, Artist, Collection, Format, Track};
use ratatui::{ use ratatui::{
backend::Backend, backend::Backend,
@ -9,10 +11,31 @@ use ratatui::{
use super::{lib::IMusicHoard, Error}; use super::{lib::IMusicHoard, Error};
#[derive(Debug)]
pub enum UiError {
Lib(String),
}
impl fmt::Display for UiError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Self::Lib(ref s) => write!(f, "the musichoard library returned an error: {s}"),
}
}
}
impl From<musichoard::Error> for UiError {
fn from(err: musichoard::Error) -> UiError {
UiError::Lib(err.to_string())
}
}
pub trait IUi { pub trait IUi {
fn is_running(&self) -> bool; fn is_running(&self) -> bool;
fn quit(&mut self); fn quit(&mut self);
fn save(&mut self) -> Result<(), UiError>;
fn increment_category(&mut self); fn increment_category(&mut self);
fn decrement_category(&mut self); fn decrement_category(&mut self);
@ -440,6 +463,7 @@ impl<'a, 'b> TrackState<'a, 'b> {
impl<MH: IMusicHoard> Ui<MH> { impl<MH: IMusicHoard> Ui<MH> {
pub fn new(mut music_hoard: MH) -> Result<Self, Error> { pub fn new(mut music_hoard: MH) -> Result<Self, Error> {
music_hoard.load_from_database()?;
music_hoard.rescan_library()?; music_hoard.rescan_library()?;
let selection = Selection::new(Some(music_hoard.get_collection())); let selection = Selection::new(Some(music_hoard.get_collection()));
Ok(Ui { Ok(Ui {
@ -531,6 +555,11 @@ impl<MH: IMusicHoard> IUi for Ui<MH> {
self.running = false; self.running = false;
} }
fn save(&mut self) -> Result<(), UiError> {
self.music_hoard.save_to_database()?;
Ok(())
}
fn increment_category(&mut self) { fn increment_category(&mut self) {
self.selection.increment_category(); self.selection.increment_category();
} }