Replace Query and QueryOption with better API (#35)

Closes #34

Reviewed-on: https://git.wojciechkozlowski.eu/wojtek/musichoard/pulls/35
This commit is contained in:
Wojciech Kozlowski 2023-04-14 10:24:24 +02:00
parent 3e9c84656e
commit 3ea04d90a6
4 changed files with 116 additions and 159 deletions

View File

@ -75,7 +75,7 @@ impl<LIB: Library, DB: Database> MhCollectionManager<LIB, DB> {
impl<LIB: Library, DB: Database> CollectionManager for MhCollectionManager<LIB, DB> {
fn rescan_library(&mut self) -> Result<(), Error> {
self.collection = self.library.list(&Query::default())?;
self.collection = self.library.list(&Query::new())?;
Ok(())
}
@ -107,7 +107,7 @@ mod tests {
let mut library = MockLibrary::new();
let mut database = MockDatabase::new();
let library_input = Query::default();
let library_input = Query::new();
let library_result = Ok(COLLECTION.to_owned());
let database_input = COLLECTION.to_owned();

View File

@ -3,7 +3,6 @@
use std::{
collections::{HashMap, HashSet},
fmt::Display,
path::{Path, PathBuf},
process::Command,
str,
@ -14,7 +13,7 @@ use mockall::automock;
use crate::{Album, AlbumId, Artist, ArtistId, Track, TrackFormat};
use super::{Error, Library, Query, QueryOption};
use super::{Error, Field, Library, Query};
macro_rules! list_format_separator {
() => {
@ -43,71 +42,35 @@ const LIST_FORMAT_ARG: &str = concat!(
const TRACK_FORMAT_FLAC: &str = "FLAC";
const TRACK_FORMAT_MP3: &str = "MP3";
trait QueryOptionArgBeets {
fn to_arg(&self, option_name: &str) -> Option<String>;
trait ToBeetsArg {
fn to_arg(&self, include: bool) -> String;
}
trait QueryArgsBeets {
trait ToBeetsArgs {
fn to_args(&self) -> Vec<String>;
}
trait SimpleOption {}
impl SimpleOption for String {}
impl SimpleOption for u32 {}
impl<T: SimpleOption + Display> QueryOptionArgBeets for QueryOption<T> {
fn to_arg(&self, option_name: &str) -> Option<String> {
let (negate, value) = match self {
Self::Include(value) => ("", value),
Self::Exclude(value) => ("^", value),
Self::None => return None,
};
Some(format!("{negate}{option_name}{value}"))
impl ToBeetsArg for Field {
fn to_arg(&self, include: bool) -> String {
let negate = if include { "" } else { "^" };
match self {
Field::AlbumArtist(ref s) => format!("{negate}albumartist:{s}"),
Field::AlbumYear(ref u) => format!("{negate}year:{u}"),
Field::AlbumTitle(ref s) => format!("{negate}album:{s}"),
Field::TrackNumber(ref u) => format!("{negate}track:{u}"),
Field::TrackTitle(ref s) => format!("{negate}title:{s}"),
Field::TrackArtist(ref v) => format!("{negate}artist:{}", v.join("; ")),
Field::All(ref s) => format!("{negate}{s}"),
}
}
}
impl QueryOptionArgBeets for QueryOption<Vec<String>> {
fn to_arg(&self, option_name: &str) -> Option<String> {
let (negate, vec) = match self {
Self::Include(value) => ("", value),
Self::Exclude(value) => ("^", value),
Self::None => return None,
};
Some(format!("{negate}{option_name}{}", vec.join("; ")))
}
}
impl QueryArgsBeets for Query {
impl ToBeetsArgs for Query {
fn to_args(&self) -> Vec<String> {
let mut arguments: Vec<String> = vec![];
if let Some(album_artist) = self.album_artist.to_arg("albumartist:") {
arguments.push(album_artist);
};
if let Some(album_year) = self.album_year.to_arg("year:") {
arguments.push(album_year);
};
if let Some(album_title) = self.album_title.to_arg("album:") {
arguments.push(album_title);
};
if let Some(track_number) = self.track_number.to_arg("track:") {
arguments.push(track_number);
};
if let Some(track_title) = self.track_title.to_arg("title:") {
arguments.push(track_title);
};
if let Some(track_artist) = self.track_artist.to_arg("artist:") {
arguments.push(track_artist);
};
if let Some(all) = self.all.to_arg("") {
arguments.push(all);
}
arguments.extend(self.include.iter().map(|field| field.to_arg(true)));
arguments.extend(self.exclude.iter().map(|field| field.to_arg(false)));
arguments
}
@ -328,41 +291,45 @@ mod tests {
#[test]
fn test_query() {
let query = Query::new()
.album_title(QueryOption::Exclude(String::from("some.album")))
.track_number(QueryOption::Include(5))
.track_artist(QueryOption::Include(vec![
let mut query = Query::new()
.exclude(Field::AlbumTitle(String::from("some.album")))
.include(Field::TrackNumber(5))
.include(Field::TrackArtist(vec![
String::from("some.artist.1"),
String::from("some.artist.2"),
]))
.all(QueryOption::Exclude(String::from("some.all")));
.exclude(Field::All(String::from("some.all")))
.to_args();
query.sort();
assert_eq!(
query.to_args(),
query,
vec![
String::from("^album:some.album"),
String::from("track:5"),
String::from("artist:some.artist.1; some.artist.2"),
String::from("^some.all"),
String::from("artist:some.artist.1; some.artist.2"),
String::from("track:5"),
]
);
let query = Query::new()
.album_artist(QueryOption::Exclude(String::from("some.albumartist")))
.album_year(QueryOption::Include(3030))
.track_title(QueryOption::Include(String::from("some.track")))
.track_artist(QueryOption::Exclude(vec![
let mut query = Query::default()
.exclude(Field::AlbumArtist(String::from("some.albumartist")))
.include(Field::AlbumYear(3030))
.include(Field::TrackTitle(String::from("some.track")))
.exclude(Field::TrackArtist(vec![
String::from("some.artist.1"),
String::from("some.artist.2"),
]));
]))
.to_args();
query.sort();
assert_eq!(
query.to_args(),
query,
vec![
String::from("^albumartist:some.albumartist"),
String::from("year:3030"),
String::from("title:some.track"),
String::from("^artist:some.artist.1; some.artist.2"),
String::from("title:some.track"),
String::from("year:3030"),
]
);
}
@ -380,7 +347,7 @@ mod tests {
.return_once(|_| result);
let mut beets = BeetsLibrary::new(executor);
let output = beets.list(&Query::default()).unwrap();
let output = beets.list(&Query::new()).unwrap();
let expected: Vec<Artist> = vec![];
assert_eq!(output, expected);
@ -400,7 +367,7 @@ mod tests {
.return_once(|_| result);
let mut beets = BeetsLibrary::new(executor);
let output = beets.list(&Query::default()).unwrap();
let output = beets.list(&Query::new()).unwrap();
assert_eq!(output, expected);
}
@ -435,7 +402,7 @@ mod tests {
.return_once(|_| result);
let mut beets = BeetsLibrary::new(executor);
let output = beets.list(&Query::default()).unwrap();
let output = beets.list(&Query::new()).unwrap();
assert_eq!(output, expected);
}
@ -457,31 +424,36 @@ mod tests {
.return_once(|_| result);
let mut beets = BeetsLibrary::new(executor);
let output = beets.list(&Query::default()).unwrap();
let output = beets.list(&Query::new()).unwrap();
assert_eq!(output, expected);
}
#[test]
fn test_list_query() {
let query = Query::new()
.album_title(QueryOption::Exclude(String::from("some.album")))
.track_number(QueryOption::Include(5))
.track_artist(QueryOption::Include(vec![String::from("some.artist")]));
let mut query = Query::new();
query
.exclude(Field::AlbumTitle(String::from("some.album")))
.include(Field::TrackNumber(5))
.include(Field::TrackArtist(vec![String::from("some.artist")]));
let arguments = vec![
"ls".to_string(),
LIST_FORMAT_ARG.to_string(),
String::from("^album:some.album"),
String::from("track:5"),
String::from("artist:some.artist"),
String::from("track:5"),
];
let result = Ok(vec![]);
let mut executor = MockBeetsLibraryExecutor::new();
executor
.expect_exec()
.with(predicate::eq(arguments))
.with(predicate::function(move |x: &[String]| {
let mut y = x.to_owned();
y[2..].sort();
y == arguments
}))
.times(1)
.return_once(|_| result);
@ -513,7 +485,7 @@ mod tests {
.return_once(|_| result);
let mut beets = BeetsLibrary::new(executor);
let err = beets.list(&Query::default()).unwrap_err();
let err = beets.list(&Query::new()).unwrap_err();
assert_eq!(err, Error::Invalid(invalid_string));
}
@ -544,7 +516,7 @@ mod tests {
.return_once(|_| result);
let mut beets = BeetsLibrary::new(executor);
let err = beets.list(&Query::default()).unwrap_err();
let err = beets.list(&Query::new()).unwrap_err();
assert_eq!(err, Error::Invalid(invalid_string));
}

View File

@ -1,6 +1,6 @@
//! Module for interacting with the music library.
use std::{fmt, num::ParseIntError, str::Utf8Error};
use std::{collections::HashSet, fmt, num::ParseIntError, str::Utf8Error};
#[cfg(test)]
use mockall::automock;
@ -9,81 +9,50 @@ use crate::Artist;
pub mod beets;
/// A single query option.
/// Individual fields that can be queried on.
#[derive(Debug, Hash, PartialEq, Eq)]
pub enum Field {
AlbumArtist(String),
AlbumYear(u32),
AlbumTitle(String),
TrackNumber(u32),
TrackTitle(String),
TrackArtist(Vec<String>),
All(String),
}
/// A library query. Can include or exclude particular fields.
#[derive(Debug, PartialEq, Eq)]
pub enum QueryOption<T> {
/// Inclusive query.
Include(T),
/// Exclusive query.
Exclude(T),
/// No query.
None,
}
impl<T> Default for QueryOption<T> {
/// Create a [`QueryOption::None`] for type `T`.
fn default() -> Self {
Self::None
}
}
/// Options for refining library queries.
#[derive(Debug, Default, PartialEq, Eq)]
pub struct Query {
album_artist: QueryOption<String>,
album_year: QueryOption<u32>,
album_title: QueryOption<String>,
track_number: QueryOption<u32>,
track_title: QueryOption<String>,
track_artist: QueryOption<Vec<String>>,
all: QueryOption<String>,
include: HashSet<Field>,
exclude: HashSet<Field>,
}
impl Default for Query {
/// Create an empty query.
fn default() -> Self {
Self::new()
}
}
impl Query {
/// Create an empty query.
pub fn new() -> Self {
Query::default()
Query {
include: HashSet::new(),
exclude: HashSet::new(),
}
}
/// Refine the query to a specific album artist.
pub fn album_artist(mut self, album_artist: QueryOption<String>) -> Self {
self.album_artist = album_artist;
/// Refine the query to include a particular search term.
pub fn include(&mut self, field: Field) -> &mut Self {
self.include.insert(field);
self
}
/// Refine the query to a specific album year.
pub fn album_year(mut self, album_year: QueryOption<u32>) -> Self {
self.album_year = album_year;
self
}
/// Refine the query to a specific album title.
pub fn album_title(mut self, album_title: QueryOption<String>) -> Self {
self.album_title = album_title;
self
}
/// Refine the query to a specific track number.
pub fn track_number(mut self, track_number: QueryOption<u32>) -> Self {
self.track_number = track_number;
self
}
/// Refine the query to a specific track title.
pub fn track_title(mut self, track_title: QueryOption<String>) -> Self {
self.track_title = track_title;
self
}
/// Refine the query to a specific set of track artists.
pub fn track_artist(mut self, track_artist: QueryOption<Vec<String>>) -> Self {
self.track_artist = track_artist;
self
}
/// Refine the query for all fields.
pub fn all(mut self, all: QueryOption<String>) -> Self {
self.all = all;
/// Refine the query to exclude a particular search term.
pub fn exclude(&mut self, field: Field) -> &mut Self {
self.exclude.insert(field);
self
}
}
@ -144,7 +113,23 @@ pub trait Library {
mod tests {
use std::io;
use super::Error;
use super::{Error, Field, Query};
#[test]
fn query() {
let mut lhs = Query::new();
let mut rhs = Query::new();
assert_eq!(
lhs.include(Field::AlbumArtist(String::from("some.artist")))
.exclude(Field::TrackTitle(String::from("some.title")))
.exclude(Field::TrackTitle(String::from("some.title")))
.include(Field::TrackNumber(6)),
rhs.exclude(Field::TrackTitle(String::from("some.title")))
.include(Field::TrackNumber(6))
.include(Field::AlbumArtist(String::from("some.artist")))
.include(Field::AlbumArtist(String::from("some.artist"))),
)
}
#[test]
fn errors() {

View File

@ -9,7 +9,7 @@ use once_cell::sync::Lazy;
use musichoard::{
library::{
beets::{BeetsLibrary, BeetsLibraryCommandExecutor},
Library, Query, QueryOption,
Field, Library, Query,
},
Artist,
};
@ -37,7 +37,7 @@ fn test_no_config_list() {
let beets_arc = BEETS_EMPTY_CONFIG.clone();
let beets = &mut beets_arc.lock().unwrap();
let output = beets.list(&Query::default()).unwrap();
let output = beets.list(&Query::new()).unwrap();
let expected: Vec<Artist> = vec![];
assert_eq!(output, expected);
@ -49,7 +49,7 @@ fn test_invalid_config() {
&PathBuf::from("./tests/files/library/config-does-not-exist.yml"),
)));
let result = beets.list(&Query::default());
let result = beets.list(&Query::new());
assert!(result.is_err());
assert!(!result.unwrap_err().to_string().is_empty());
}
@ -59,7 +59,7 @@ fn test_full_list() {
let beets_arc = BEETS_TEST_CONFIG.clone();
let beets = &mut beets_arc.lock().unwrap();
let output = beets.list(&Query::default()).unwrap();
let output = beets.list(&Query::new()).unwrap();
let expected: Vec<Artist> = COLLECTION.to_owned();
assert_eq!(output, expected);
@ -71,7 +71,7 @@ fn test_album_artist_query() {
let beets = &mut beets_arc.lock().unwrap();
let output = beets
.list(&Query::default().album_artist(QueryOption::Include(String::from("Аркона"))))
.list(Query::new().include(Field::AlbumArtist(String::from("Аркона"))))
.unwrap();
let expected: Vec<Artist> = COLLECTION[0..1].to_owned();
@ -84,7 +84,7 @@ fn test_album_title_query() {
let beets = &mut beets_arc.lock().unwrap();
let output = beets
.list(&Query::default().album_title(QueryOption::Include(String::from("Slovo"))))
.list(&Query::new().include(Field::AlbumTitle(String::from("Slovo"))))
.unwrap();
let expected: Vec<Artist> = COLLECTION[0..1].to_owned();
@ -97,7 +97,7 @@ fn test_exclude_query() {
let beets = &mut beets_arc.lock().unwrap();
let output = beets
.list(&Query::default().album_artist(QueryOption::Exclude(String::from("Аркона"))))
.list(&Query::new().exclude(Field::AlbumArtist(String::from("Аркона"))))
.unwrap();
let expected: Vec<Artist> = COLLECTION[1..].to_owned();