Split lib.rs into smaller files (#115)
All checks were successful
Cargo CI / Build and Test (push) Successful in 1m2s
Cargo CI / Lint (push) Successful in 44s
Cargo CI / Build and Test (pull_request) Successful in 2m23s
Cargo CI / Lint (pull_request) Successful in 44s

Closes #110

Reviewed-on: #115
This commit is contained in:
Wojciech Kozlowski 2024-01-22 23:01:34 +01:00
parent 6e9249e265
commit ba85505c9a
37 changed files with 2604 additions and 2360 deletions

View File

@ -1,10 +1,12 @@
use paste::paste;
use std::path::PathBuf;
use paste::paste;
use structopt::{clap::AppSettings, StructOpt};
use musichoard::{
collection::artist::ArtistId,
database::json::{backend::JsonDatabaseFileBackend, JsonDatabase},
ArtistId, MusicHoard, MusicHoardBuilder, NoLibrary,
MusicHoard, MusicHoardBuilder, NoLibrary,
};
type MH = MusicHoard<NoLibrary, JsonDatabase<JsonDatabaseFileBackend>>;

View File

@ -0,0 +1,84 @@
use std::mem;
use serde::{Deserialize, Serialize};
use crate::core::collection::{
merge::{Merge, MergeSorted},
track::Track,
};
/// An album is a collection of tracks that were released together.
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub struct Album {
pub id: AlbumId,
pub tracks: Vec<Track>,
}
/// The album identifier.
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq, Hash)]
pub struct AlbumId {
pub year: u32,
pub title: String,
}
impl PartialOrd for Album {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Album {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.id.cmp(&other.id)
}
}
impl Merge for Album {
fn merge_in_place(&mut self, other: Self) {
assert_eq!(self.id, other.id);
let tracks = mem::take(&mut self.tracks);
self.tracks = MergeSorted::new(tracks.into_iter(), other.tracks.into_iter()).collect();
}
}
#[cfg(test)]
mod tests {
use crate::core::testmod::FULL_COLLECTION;
use super::*;
#[test]
fn merge_album_no_overlap() {
let left = FULL_COLLECTION[0].albums[0].to_owned();
let mut right = FULL_COLLECTION[0].albums[1].to_owned();
right.id = left.id.clone();
let mut expected = left.clone();
expected.tracks.append(&mut right.tracks.clone());
expected.tracks.sort_unstable();
let merged = left.clone().merge(right.clone());
assert_eq!(expected, merged);
// Non-overlapping merge should be commutative.
let merged = right.clone().merge(left.clone());
assert_eq!(expected, merged);
}
#[test]
fn merge_album_overlap() {
let mut left = FULL_COLLECTION[0].albums[0].to_owned();
let mut right = FULL_COLLECTION[0].albums[1].to_owned();
right.id = left.id.clone();
left.tracks.push(right.tracks[0].clone());
left.tracks.sort_unstable();
let mut expected = left.clone();
expected.tracks.append(&mut right.tracks.clone());
expected.tracks.sort_unstable();
expected.tracks.dedup();
let merged = left.clone().merge(right);
assert_eq!(expected, merged);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,67 @@
use std::{cmp::Ordering, iter::Peekable};
/// A trait for merging two objects. The merge is asymmetric with the left argument considered to be
/// the primary whose properties are to be kept in case of collisions.
pub trait Merge {
fn merge_in_place(&mut self, other: Self);
fn merge(mut self, other: Self) -> Self
where
Self: Sized,
{
self.merge_in_place(other);
self
}
fn merge_vecs<T: Ord + Eq>(this: &mut Vec<T>, mut other: Vec<T>) {
this.append(&mut other);
this.sort_unstable();
this.dedup();
}
}
pub struct MergeSorted<L, R>
where
L: Iterator<Item = R::Item>,
R: Iterator,
{
left: Peekable<L>,
right: Peekable<R>,
}
impl<L, R> MergeSorted<L, R>
where
L: Iterator<Item = R::Item>,
R: Iterator,
{
pub fn new(left: L, right: R) -> MergeSorted<L, R> {
MergeSorted {
left: left.peekable(),
right: right.peekable(),
}
}
}
impl<L, R> Iterator for MergeSorted<L, R>
where
L: Iterator<Item = R::Item>,
R: Iterator,
L::Item: Ord + Merge,
{
type Item = L::Item;
fn next(&mut self) -> Option<L::Item> {
let which = match (self.left.peek(), self.right.peek()) {
(Some(l), Some(r)) => l.cmp(r),
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
(None, None) => return None,
};
match which {
Ordering::Less => self.left.next(),
Ordering::Equal => Some(self.left.next().unwrap().merge(self.right.next().unwrap())),
Ordering::Greater => self.right.next(),
}
}
}

View File

@ -0,0 +1,40 @@
//! The collection module defines the core data types and their relations.
pub mod album;
pub mod artist;
pub mod track;
mod merge;
pub use merge::Merge;
use std::fmt::{self, Display};
/// The [`Collection`] alias type for convenience.
pub type Collection = Vec<artist::Artist>;
/// Error type for the [`collection`] module.
#[derive(Debug, PartialEq, Eq)]
pub enum Error {
/// An error occurred when processing a URL.
UrlError(String),
}
impl Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Self::UrlError(ref s) => write!(f, "an error occurred when processing a URL: {s}"),
}
}
}
impl From<url::ParseError> for Error {
fn from(err: url::ParseError) -> Error {
Error::UrlError(err.to_string())
}
}
impl From<uuid::Error> for Error {
fn from(err: uuid::Error) -> Error {
Error::UrlError(err.to_string())
}
}

View File

@ -0,0 +1,81 @@
use serde::{Deserialize, Serialize};
use crate::core::collection::merge::Merge;
/// A single track on an album.
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub struct Track {
pub id: TrackId,
pub artist: Vec<String>,
pub quality: Quality,
}
/// The track identifier.
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct TrackId {
pub number: u32,
pub title: String,
}
/// The track quality. Combines format and bitrate information.
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub struct Quality {
pub format: Format,
pub bitrate: u32,
}
/// The track file format.
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)]
pub enum Format {
Flac,
Mp3,
}
impl PartialOrd for Track {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Track {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.id.cmp(&other.id)
}
}
impl Merge for Track {
fn merge_in_place(&mut self, other: Self) {
assert_eq!(self.id, other.id);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn merge_track() {
let left = Track {
id: TrackId {
number: 4,
title: String::from("a title"),
},
artist: vec![String::from("left artist")],
quality: Quality {
format: Format::Flac,
bitrate: 1411,
},
};
let right = Track {
id: left.id.clone(),
artist: vec![String::from("right artist")],
quality: Quality {
format: Format::Mp3,
bitrate: 320,
},
};
let merged = left.clone().merge(right);
assert_eq!(left, merged);
}
}

View File

@ -3,7 +3,7 @@
use std::fs;
use std::path::PathBuf;
use super::IJsonDatabaseBackend;
use crate::core::database::json::IJsonDatabaseBackend;
/// JSON database backend that uses a local file for persistent storage.
pub struct JsonDatabaseFileBackend {

View File

@ -1,13 +1,14 @@
//! Module for storing MusicHoard data in a JSON file database.
pub mod backend;
#[cfg(test)]
use mockall::automock;
use crate::Collection;
use super::{IDatabase, LoadError, SaveError};
pub mod backend;
use crate::core::{
collection::Collection,
database::{IDatabase, LoadError, SaveError},
};
impl From<serde_json::Error> for LoadError {
fn from(err: serde_json::Error) -> LoadError {
@ -66,7 +67,13 @@ mod tests {
use mockall::predicate;
use crate::{testlib::FULL_COLLECTION, Artist, ArtistId, Collection};
use crate::core::{
collection::{
artist::{Artist, ArtistId},
Collection,
},
testmod::FULL_COLLECTION,
};
use super::*;
use testmod::DATABASE_JSON;

View File

@ -1,14 +1,14 @@
//! Module for storing MusicHoard data in a database.
#[cfg(feature = "database-json")]
pub mod json;
use std::fmt;
#[cfg(test)]
use mockall::automock;
use crate::Collection;
#[cfg(feature = "database-json")]
pub mod json;
use crate::core::collection::Collection;
/// Trait for interacting with the database.
#[cfg_attr(test, automock)]

View File

@ -8,7 +8,7 @@ use std::{
str,
};
use super::{Error, IBeetsLibraryExecutor};
use crate::core::library::{beets::IBeetsLibraryExecutor, Error};
const BEET_DEFAULT: &str = "beet";

View File

@ -1,14 +1,15 @@
//! Module for interacting with the music library via
//! [beets](https://beets.readthedocs.io/en/stable/).
pub mod executor;
#[cfg(test)]
use mockall::automock;
use crate::Format;
use super::{Error, Field, ILibrary, Item, Query};
pub mod executor;
use crate::core::{
collection::track::Format,
library::{Error, Field, ILibrary, Item, Query},
};
macro_rules! list_format_separator {
() => {
@ -176,7 +177,7 @@ mod testmod;
mod tests {
use mockall::predicate;
use crate::library::testmod::LIBRARY_ITEMS;
use crate::core::library::testmod::LIBRARY_ITEMS;
use super::*;
use testmod::LIBRARY_BEETS;

View File

@ -1,14 +1,14 @@
//! Module for interacting with the music library.
#[cfg(feature = "library-beets")]
pub mod beets;
use std::{collections::HashSet, fmt, num::ParseIntError, str::Utf8Error};
#[cfg(test)]
use mockall::automock;
use crate::Format;
#[cfg(feature = "library-beets")]
pub mod beets;
use crate::core::collection::track::Format;
/// Trait for interacting with the music library.
#[cfg_attr(test, automock)]

View File

@ -1,6 +1,6 @@
use once_cell::sync::Lazy;
use crate::{library::Item, Format};
use crate::core::{collection::track::Format, library::Item};
pub static LIBRARY_ITEMS: Lazy<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
vec![

7
src/core/mod.rs Normal file
View File

@ -0,0 +1,7 @@
pub mod collection;
pub mod database;
pub mod library;
pub mod musichoard;
#[cfg(test)]
pub mod testmod;

View File

@ -0,0 +1,56 @@
//! The core MusicHoard module. Serves as the main entry-point into the library.
#![allow(clippy::module_inception)]
pub mod musichoard;
pub mod musichoard_builder;
use std::fmt::{self, Display};
use crate::core::{collection, database, library};
/// Error type for `musichoard`.
#[derive(Debug, PartialEq, Eq)]
pub enum Error {
/// The [`MusicHoard`] is not able to read/write its in-memory collection.
CollectionError(String),
/// 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 Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Self::CollectionError(ref s) => write!(f, "failed to read/write the collection: {s}"),
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<collection::Error> for Error {
fn from(err: collection::Error) -> Self {
Error::CollectionError(err.to_string())
}
}
impl From<library::Error> for Error {
fn from(err: library::Error) -> Error {
Error::LibraryError(err.to_string())
}
}
impl From<database::LoadError> for Error {
fn from(err: database::LoadError) -> Error {
Error::DatabaseError(err.to_string())
}
}
impl From<database::SaveError> for Error {
fn from(err: database::SaveError) -> Error {
Error::DatabaseError(err.to_string())
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,92 @@
use crate::core::{
database::IDatabase,
library::ILibrary,
musichoard::musichoard::{MusicHoard, NoDatabase, NoLibrary},
};
/// Builder for [`MusicHoard`]. Its purpose is to make it easier to set various combinations of
/// library/database or their absence.
pub struct MusicHoardBuilder<LIB, DB> {
library: LIB,
database: DB,
}
impl Default for MusicHoardBuilder<NoLibrary, NoDatabase> {
/// Create a [`MusicHoardBuilder`].
fn default() -> Self {
Self::new()
}
}
impl MusicHoardBuilder<NoLibrary, NoDatabase> {
/// Create a [`MusicHoardBuilder`].
pub fn new() -> Self {
MusicHoardBuilder {
library: NoLibrary,
database: NoDatabase,
}
}
}
impl<LIB, DB> MusicHoardBuilder<LIB, DB> {
/// Set a library for [`MusicHoard`].
pub fn set_library<NEWLIB: ILibrary>(self, library: NEWLIB) -> MusicHoardBuilder<NEWLIB, DB> {
MusicHoardBuilder {
library,
database: self.database,
}
}
/// Set a database for [`MusicHoard`].
pub fn set_database<NEWDB: IDatabase>(self, database: NEWDB) -> MusicHoardBuilder<LIB, NEWDB> {
MusicHoardBuilder {
library: self.library,
database,
}
}
/// Build [`MusicHoard`] with the currently set library and database.
pub fn build(self) -> MusicHoard<LIB, DB> {
MusicHoard::new(self.library, self.database)
}
}
#[cfg(test)]
mod tests {
use crate::core::{database::NullDatabase, library::NullLibrary};
use super::*;
#[test]
fn no_library_no_database() {
MusicHoardBuilder::default();
}
#[test]
fn with_library_no_database() {
let mut mh = MusicHoardBuilder::default()
.set_library(NullLibrary)
.build();
assert!(mh.rescan_library().is_ok());
}
#[test]
fn no_library_with_database() {
let mut mh = MusicHoardBuilder::default()
.set_database(NullDatabase)
.build();
assert!(mh.load_from_database().is_ok());
assert!(mh.save_to_database().is_ok());
}
#[test]
fn with_library_with_database() {
let mut mh = MusicHoardBuilder::default()
.set_library(NullLibrary)
.set_database(NullDatabase)
.build();
assert!(mh.rescan_library().is_ok());
assert!(mh.load_from_database().is_ok());
assert!(mh.save_to_database().is_ok());
}
}

11
src/core/testmod.rs Normal file
View File

@ -0,0 +1,11 @@
use once_cell::sync::Lazy;
use crate::core::collection::{
album::{Album, AlbumId},
artist::{Artist, ArtistId, ArtistProperties, Bandcamp, MusicBrainz, MusicButler, Qobuz},
track::{Format, Quality, Track, TrackId},
};
use crate::tests::*;
pub static LIBRARY_COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| library_collection!());
pub static FULL_COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| full_collection!());

2296
src/lib.rs

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,5 @@
mod tui;
use std::{ffi::OsString, fs::OpenOptions, io, path::PathBuf};
use ratatui::{backend::CrosstermBackend, Terminal};
@ -18,7 +20,6 @@ use musichoard::{
MusicHoardBuilder, NoDatabase, NoLibrary,
};
mod tui;
use tui::{event::EventChannel, handler::EventHandler, listener::EventListener, ui::Ui, Tui};
#[derive(StructOpt)]
@ -128,7 +129,4 @@ fn main() {
#[cfg(test)]
#[macro_use]
mod testmacros;
#[cfg(test)]
mod testbin;
mod tests;

View File

@ -1,7 +0,0 @@
use musichoard::{
Album, AlbumId, Artist, ArtistId, ArtistProperties, Bandcamp, Format, MusicBrainz, MusicButler,
Qobuz, Quality, Track, TrackId,
};
use once_cell::sync::Lazy;
pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| full_collection!());

View File

@ -1,8 +0,0 @@
use crate::{
Album, AlbumId, Artist, ArtistId, ArtistProperties, Bandcamp, Format, MusicBrainz, MusicButler,
Qobuz, Quality, Track, TrackId,
};
use once_cell::sync::Lazy;
pub static LIBRARY_COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| library_collection!());
pub static FULL_COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| full_collection!());

View File

@ -321,3 +321,6 @@ macro_rules! full_collection {
collection
}};
}
pub(crate) use full_collection;
pub(crate) use library_collection;

View File

@ -2,7 +2,7 @@ use crossterm::event::{KeyEvent, MouseEvent};
use std::fmt;
use std::sync::mpsc;
use super::ui::UiError;
use crate::tui::ui::UiError;
#[derive(Debug)]
pub enum EventError {
@ -104,7 +104,7 @@ mod tests {
use crate::tui::ui::UiError;
use super::{Event, EventChannel, EventError};
use super::*;
#[test]
fn event_sender() {

View File

@ -3,7 +3,7 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
#[cfg(test)]
use mockall::automock;
use super::{
use crate::tui::{
event::{Event, EventError, EventReceiver},
ui::IUi,
};

View File

@ -1,4 +1,4 @@
use musichoard::{database::IDatabase, library::ILibrary, Collection, MusicHoard};
use musichoard::{collection::Collection, database::IDatabase, library::ILibrary, MusicHoard};
#[cfg(test)]
use mockall::automock;

View File

@ -157,24 +157,24 @@ impl<B: Backend, UI: IUi> Tui<B, UI> {
// GRCOV_EXCL_STOP
}
#[cfg(test)]
mod testmod;
#[cfg(test)]
mod tests {
use std::{io, thread};
use musichoard::Collection;
use ratatui::{backend::TestBackend, Terminal};
use crate::testbin::COLLECTION;
use musichoard::collection::Collection;
use super::{
event::EventError,
handler::MockIEventHandler,
lib::MockIMusicHoard,
listener::MockIEventListener,
ui::{IUi, Ui},
Error, Tui,
use crate::tui::{
handler::MockIEventHandler, lib::MockIMusicHoard, listener::MockIEventListener, ui::Ui,
};
use super::*;
use testmod::COLLECTION;
pub fn terminal() -> Terminal<TestBackend> {
let backend = TestBackend::new(150, 30);
Terminal::new(backend).unwrap()

10
src/tui/testmod.rs Normal file
View File

@ -0,0 +1,10 @@
use musichoard::collection::{
album::{Album, AlbumId},
artist::{Artist, ArtistId, ArtistProperties, Bandcamp, MusicBrainz, MusicButler, Qobuz},
track::{Format, Quality, Track, TrackId},
};
use once_cell::sync::Lazy;
use crate::tests::*;
pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| full_collection!());

View File

@ -1,6 +1,11 @@
use std::fmt;
use musichoard::{Album, Artist, Collection, Format, Track};
use musichoard::collection::{
album::Album,
artist::Artist,
track::{Format, Track},
Collection,
};
use ratatui::{
backend::Backend,
layout::{Alignment, Rect},
@ -9,7 +14,7 @@ use ratatui::{
Frame,
};
use super::{lib::IMusicHoard, Error};
use crate::tui::{lib::IMusicHoard, Error};
#[derive(Debug)]
pub enum UiError {
@ -733,8 +738,8 @@ impl<MH: IMusicHoard> IUi for Ui<MH> {
#[cfg(test)]
mod tests {
use crate::testbin::COLLECTION;
use crate::tui::lib::MockIMusicHoard;
use crate::tui::testmod::COLLECTION;
use crate::tui::tests::{terminal, ui};
use super::*;

View File

@ -1,14 +1,15 @@
use std::{fs, path::PathBuf};
use once_cell::sync::Lazy;
use tempfile::NamedTempFile;
use musichoard::{
collection::artist::Artist,
database::{
json::{backend::JsonDatabaseFileBackend, JsonDatabase},
IDatabase,
},
Artist,
};
use once_cell::sync::Lazy;
use tempfile::NamedTempFile;
use crate::testlib::COLLECTION;

View File

@ -5,14 +5,14 @@ mod testlib;
use musichoard::MusicHoard;
use crate::testlib::COLLECTION;
#[cfg(feature = "database-json")]
use musichoard::database::json::{backend::JsonDatabaseFileBackend, JsonDatabase};
#[cfg(feature = "library-beets")]
use musichoard::library::beets::{executor::BeetsLibraryProcessExecutor, BeetsLibrary};
use crate::testlib::COLLECTION;
#[test]
#[cfg(feature = "database-json")]
#[cfg(feature = "library-beets")]

View File

@ -12,7 +12,7 @@ use musichoard::library::{
Field, ILibrary, Item, Query,
};
use super::testmod::LIBRARY_ITEMS;
use crate::library::testmod::LIBRARY_ITEMS;
pub static BEETS_TEST_CONFIG_PATH: Lazy<PathBuf> =
Lazy::new(|| fs::canonicalize("./tests/files/library/config.yml").unwrap());

View File

@ -1,4 +1,4 @@
mod testmod;
#[cfg(feature = "library-beets")]
pub mod beets;
mod testmod;

View File

@ -1,6 +1,6 @@
use once_cell::sync::Lazy;
use musichoard::{library::Item, Format};
use musichoard::{collection::track::Format, library::Item};
pub static LIBRARY_ITEMS: Lazy<Vec<Item>> = Lazy::new(|| -> Vec<Item> {
vec![

View File

@ -1,9 +1,12 @@
use musichoard::{
Album, AlbumId, Artist, ArtistId, ArtistProperties, Bandcamp, Collection, Format, MusicBrainz,
MusicButler, Qobuz, Quality, Track, TrackId,
};
use once_cell::sync::Lazy;
use musichoard::collection::{
album::{Album, AlbumId},
artist::{Artist, ArtistId, ArtistProperties, Bandcamp, MusicBrainz, MusicButler, Qobuz},
track::{Format, Quality, Track, TrackId},
Collection,
};
pub static COLLECTION: Lazy<Vec<Artist>> = Lazy::new(|| -> Collection {
vec![
Artist {