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
/codecov
database.json

View File

@ -6,13 +6,19 @@ use serde::Serialize;
#[cfg(test)]
use mockall::automock;
use super::{Error, IDatabase};
use super::{IDatabase, ReadError, WriteError};
pub mod backend;
impl From<serde_json::Error> for Error {
fn from(err: serde_json::Error) -> Error {
Error::SerDeError(err.to_string())
impl From<serde_json::Error> for ReadError {
fn from(err: serde_json::Error) -> ReadError {
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> {
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()?;
*collection = serde_json::from_str(&serialized)?;
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)?;
self.backend.write(&serialized)?;
Ok(())
@ -191,7 +197,7 @@ mod tests {
let serde_err = serde_json::to_string(&object);
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!(!format!("{:?}", serde_err).is_empty());
}

View File

@ -14,35 +14,61 @@ pub mod json;
#[cfg_attr(test, automock)]
pub trait IDatabase {
/// 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.
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.
#[derive(Debug)]
pub enum Error {
/// The database experienced an I/O error.
pub enum ReadError {
/// The database experienced an I/O read error.
IoError(String),
/// The database experienced a (de)serialisation error.
/// The database experienced a deserialisation error.
SerDeError(String),
}
impl fmt::Display for Error {
impl fmt::Display for ReadError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
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) => {
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 {
fn from(err: std::io::Error) -> Error {
Error::IoError(err.to_string())
impl From<std::io::Error> for ReadError {
fn from(err: std::io::Error) -> ReadError {
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 {
use std::io;
use super::Error;
use super::{ReadError, WriteError};
#[test]
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!(!format!("{:?}", io_err).is_empty());
}

View File

@ -7,7 +7,8 @@ use std::{
cmp::Ordering,
collections::{HashMap, HashSet},
fmt,
iter::Peekable, mem,
iter::Peekable,
mem,
};
use database::IDatabase;
@ -212,8 +213,14 @@ impl From<library::Error> for Error {
}
}
impl From<database::Error> for Error {
fn from(err: database::Error) -> Error {
impl From<database::ReadError> for 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())
}
}
@ -521,7 +528,7 @@ mod tests {
let library = MockILibrary::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
.expect_write()
@ -531,8 +538,9 @@ mod tests {
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());
let expected_err = Error::DatabaseError(
database::WriteError::IoError(String::from("I/O error")).to_string(),
);
assert_eq!(actual_err, expected_err);
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::{ffi::OsString, io};
use musichoard::Collection;
use ratatui::{backend::CrosstermBackend, Terminal};
use structopt::StructOpt;
@ -57,9 +59,28 @@ fn with<LIB: ILibrary, DB: IDatabase>(lib: LIB, db: DB) {
}
fn main() {
// Create the application.
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 {
let uri = uri.into_string().expect("invalid SSH URI");
let beets_config_file_path = opt
@ -70,11 +91,9 @@ fn main() {
let lib_exec = BeetsLibrarySshExecutor::new(uri)
.expect("failed to initialise beets")
.config(beets_config_file_path);
let db_exec = JsonDatabaseFileBackend::new(&opt.database_file_path);
with(BeetsLibrary::new(lib_exec), JsonDatabase::new(db_exec));
} else {
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));
}
}

View File

@ -2,11 +2,14 @@ use crossterm::event::{KeyEvent, MouseEvent};
use std::fmt;
use std::sync::mpsc;
use super::ui::UiError;
#[derive(Debug)]
pub enum EventError {
Send(Event),
Recv,
Io(std::io::Error),
Ui(String),
}
impl fmt::Display for EventError {
@ -17,6 +20,9 @@ impl fmt::Display for EventError {
Self::Io(ref 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)]
pub enum Event {
Key(KeyEvent),

View File

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

View File

@ -3,24 +3,26 @@ 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 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;
}
impl From<musichoard::Error> for Error {
fn from(err: musichoard::Error) -> Error {
Error::Lib(err.to_string())
}
}
// GRCOV_EXCL_START
impl<LIB: ILibrary, DB: IDatabase> IMusicHoard for MusicHoard<LIB, DB> {
fn rescan_library(&mut self) -> Result<(), Error> {
Ok(MusicHoard::rescan_library(self)?)
fn rescan_library(&mut self) -> Result<(), musichoard::Error> {
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 {

View File

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

View File

@ -1,3 +1,5 @@
use std::fmt;
use musichoard::{Album, Artist, Collection, Format, Track};
use ratatui::{
backend::Backend,
@ -9,10 +11,31 @@ use ratatui::{
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 {
fn is_running(&self) -> bool;
fn quit(&mut self);
fn save(&mut self) -> Result<(), UiError>;
fn increment_category(&mut self);
fn decrement_category(&mut self);
@ -440,6 +463,7 @@ impl<'a, 'b> TrackState<'a, 'b> {
impl<MH: IMusicHoard> Ui<MH> {
pub fn new(mut music_hoard: MH) -> Result<Self, Error> {
music_hoard.load_from_database()?;
music_hoard.rescan_library()?;
let selection = Selection::new(Some(music_hoard.get_collection()));
Ok(Ui {
@ -531,6 +555,11 @@ impl<MH: IMusicHoard> IUi for Ui<MH> {
self.running = false;
}
fn save(&mut self) -> Result<(), UiError> {
self.music_hoard.save_to_database()?;
Ok(())
}
fn increment_category(&mut self) {
self.selection.increment_category();
}