Replace Query and QueryOption with better API #35

Merged
wojtek merged 3 commits from 34---replace-query-and-queryoption-with-better-api into main 2023-04-14 10:24:24 +02:00
4 changed files with 108 additions and 158 deletions
Showing only changes of commit 9192c4bdb4 - Show all commits

View File

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

View File

@ -3,7 +3,6 @@
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
fmt::Display,
path::{Path, PathBuf}, path::{Path, PathBuf},
process::Command, process::Command,
str, str,
@ -14,7 +13,7 @@ use mockall::automock;
use crate::{Album, AlbumId, Artist, ArtistId, Track, TrackFormat}; 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 { macro_rules! list_format_separator {
() => { () => {
@ -43,70 +42,39 @@ const LIST_FORMAT_ARG: &str = concat!(
const TRACK_FORMAT_FLAC: &str = "FLAC"; const TRACK_FORMAT_FLAC: &str = "FLAC";
const TRACK_FORMAT_MP3: &str = "MP3"; const TRACK_FORMAT_MP3: &str = "MP3";
trait QueryOptionArgBeets { trait ToBeetsArg {
fn to_arg(&self, option_name: &str) -> Option<String>; fn to_arg(&self, include: bool) -> String;
} }
trait QueryArgsBeets { trait ToBeetsArgs {
fn to_args(&self) -> Vec<String>; fn to_args(&self) -> Vec<String>;
} }
trait SimpleOption {} impl ToBeetsArg for Field {
impl SimpleOption for String {} fn to_arg(&self, include: bool) -> String {
impl SimpleOption for u32 {} let negate = if include { "" } else { "^" };
match self {
impl<T: SimpleOption + Display> QueryOptionArgBeets for QueryOption<T> { Field::AlbumArtist(ref s) => format!("{negate}albumartist:{s}"),
fn to_arg(&self, option_name: &str) -> Option<String> { Field::AlbumYear(ref u) => format!("{negate}year:{u}"),
let (negate, value) = match self { Field::AlbumTitle(ref s) => format!("{negate}album:{s}"),
Self::Include(value) => ("", value), Field::TrackNumber(ref u) => format!("{negate}track:{u}"),
Self::Exclude(value) => ("^", value), Field::TrackTitle(ref s) => format!("{negate}title:{s}"),
Self::None => return None, Field::TrackArtist(ref v) => format!("{negate}artist:{}", v.join("; ")),
}; Field::All(ref s) => format!("{negate}{s}"),
Some(format!("{negate}{option_name}{value}")) }
} }
} }
impl QueryOptionArgBeets for QueryOption<Vec<String>> { impl ToBeetsArgs for Query {
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 {
fn to_args(&self) -> Vec<String> { fn to_args(&self) -> Vec<String> {
let mut arguments: Vec<String> = vec![]; let mut arguments: Vec<String> = vec![];
if let Some(album_artist) = self.album_artist.to_arg("albumartist:") { for field in self.include.iter() {
arguments.push(album_artist); arguments.push(field.to_arg(true));
}; }
if let Some(album_year) = self.album_year.to_arg("year:") { for field in self.exclude.iter() {
arguments.push(album_year); arguments.push(field.to_arg(false));
};
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 arguments
@ -328,40 +296,38 @@ mod tests {
#[test] #[test]
fn test_query() { 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![
String::from("some.artist.1"),
String::from("some.artist.2"),
]))
.all(QueryOption::Exclude(String::from("some.all")));
assert_eq!( assert_eq!(
query.to_args(), 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"),
]))
.exclude(Field::All(String::from("some.all")))
.to_args(),
vec![ vec![
String::from("^album:some.album"),
String::from("track:5"), String::from("track:5"),
String::from("artist:some.artist.1; some.artist.2"), String::from("artist:some.artist.1; some.artist.2"),
String::from("^album:some.album"),
String::from("^some.all"), String::from("^some.all"),
] ]
); );
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![
String::from("some.artist.1"),
String::from("some.artist.2"),
]));
assert_eq!( assert_eq!(
query.to_args(), 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(),
vec![ vec![
String::from("^albumartist:some.albumartist"),
String::from("year:3030"), String::from("year:3030"),
String::from("title:some.track"), String::from("title:some.track"),
String::from("^albumartist:some.albumartist"),
String::from("^artist:some.artist.1; some.artist.2"), String::from("^artist:some.artist.1; some.artist.2"),
] ]
); );
@ -380,7 +346,7 @@ mod tests {
.return_once(|_| result); .return_once(|_| result);
let mut beets = BeetsLibrary::new(executor); 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![]; let expected: Vec<Artist> = vec![];
assert_eq!(output, expected); assert_eq!(output, expected);
@ -400,7 +366,7 @@ mod tests {
.return_once(|_| result); .return_once(|_| result);
let mut beets = BeetsLibrary::new(executor); let mut beets = BeetsLibrary::new(executor);
let output = beets.list(&Query::default()).unwrap(); let output = beets.list(&Query::new()).unwrap();
assert_eq!(output, expected); assert_eq!(output, expected);
} }
@ -435,7 +401,7 @@ mod tests {
.return_once(|_| result); .return_once(|_| result);
let mut beets = BeetsLibrary::new(executor); let mut beets = BeetsLibrary::new(executor);
let output = beets.list(&Query::default()).unwrap(); let output = beets.list(&Query::new()).unwrap();
assert_eq!(output, expected); assert_eq!(output, expected);
} }
@ -457,24 +423,25 @@ mod tests {
.return_once(|_| result); .return_once(|_| result);
let mut beets = BeetsLibrary::new(executor); let mut beets = BeetsLibrary::new(executor);
let output = beets.list(&Query::default()).unwrap(); let output = beets.list(&Query::new()).unwrap();
assert_eq!(output, expected); assert_eq!(output, expected);
} }
#[test] #[test]
fn test_list_query() { fn test_list_query() {
let query = Query::new() let mut query = Query::new();
.album_title(QueryOption::Exclude(String::from("some.album"))) query
.track_number(QueryOption::Include(5)) .exclude(Field::AlbumTitle(String::from("some.album")))
.track_artist(QueryOption::Include(vec![String::from("some.artist")])); .include(Field::TrackNumber(5))
.include(Field::TrackArtist(vec![String::from("some.artist")]));
let arguments = vec![ let arguments = vec![
"ls".to_string(), "ls".to_string(),
LIST_FORMAT_ARG.to_string(), LIST_FORMAT_ARG.to_string(),
String::from("^album:some.album"),
String::from("track:5"), String::from("track:5"),
String::from("artist:some.artist"), String::from("artist:some.artist"),
String::from("^album:some.album"),
]; ];
let result = Ok(vec![]); let result = Ok(vec![]);
@ -513,7 +480,7 @@ mod tests {
.return_once(|_| result); .return_once(|_| result);
let mut beets = BeetsLibrary::new(executor); 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)); assert_eq!(err, Error::Invalid(invalid_string));
} }
@ -544,7 +511,7 @@ mod tests {
.return_once(|_| result); .return_once(|_| result);
let mut beets = BeetsLibrary::new(executor); 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)); assert_eq!(err, Error::Invalid(invalid_string));
} }

View File

@ -9,81 +9,50 @@ use crate::Artist;
pub mod beets; pub mod beets;
/// A single query option. /// Individual fields that can be queried on.
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub enum QueryOption<T> { pub enum Field {
/// Inclusive query. AlbumArtist(String),
Include(T), AlbumYear(u32),
/// Exclusive query. AlbumTitle(String),
Exclude(T), TrackNumber(u32),
/// No query. TrackTitle(String),
None, TrackArtist(Vec<String>),
All(String),
} }
impl<T> Default for QueryOption<T> { /// A library query. Can include or exclude particular fields.
/// Create a [`QueryOption::None`] for type `T`. #[derive(Debug, PartialEq, Eq)]
fn default() -> Self {
Self::None
}
}
/// Options for refining library queries.
#[derive(Debug, Default, PartialEq, Eq)]
pub struct Query { pub struct Query {
album_artist: QueryOption<String>, include: Vec<Field>,
album_year: QueryOption<u32>, exclude: Vec<Field>,
album_title: QueryOption<String>, }
track_number: QueryOption<u32>,
track_title: QueryOption<String>, impl Default for Query {
track_artist: QueryOption<Vec<String>>, /// Create an empty query.
all: QueryOption<String>, fn default() -> Self {
Self::new()
}
} }
impl Query { impl Query {
/// Create an empty query. /// Create an empty query.
pub fn new() -> Self { pub fn new() -> Self {
Query::default() Query {
include: vec![],
exclude: vec![],
}
} }
/// Refine the query to a specific album artist. /// Refine the query to include a particular search term.
pub fn album_artist(mut self, album_artist: QueryOption<String>) -> Self { pub fn include(&mut self, field: Field) -> &mut Self {
self.album_artist = album_artist; self.include.push(field);
self self
} }
/// Refine the query to a specific album year. /// Refine the query to exclude a particular search term.
pub fn album_year(mut self, album_year: QueryOption<u32>) -> Self { pub fn exclude(&mut self, field: Field) -> &mut Self {
self.album_year = album_year; self.exclude.push(field);
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;
self self
} }
} }
@ -144,7 +113,21 @@ pub trait Library {
mod tests { mod tests {
use std::io; 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")))
.include(Field::TrackNumber(6)),
rhs.exclude(Field::TrackTitle(String::from("some.title")))
.include(Field::AlbumArtist(String::from("some.artist")))
.include(Field::TrackNumber(6)),
)
}
#[test] #[test]
fn errors() { fn errors() {

View File

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