Integrate changes with binary
This commit is contained in:
parent
6a90b6cf78
commit
e2dd53845f
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
/target
|
||||
/codecov
|
||||
database.json
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
20
src/lib.rs
20
src/lib.rs
@ -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());
|
||||
|
25
src/main.rs
25
src/main.rs
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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())
|
||||
|
@ -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();
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user