Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added a builder like struct for the search function (Related with issue ) #364

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
18 changes: 18 additions & 0 deletions rspotify-model/src/search.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! All object related to search

use serde::{Deserialize, Serialize};
use strum::Display;

use crate::{
FullArtist, FullTrack, Page, SimplifiedAlbum, SimplifiedEpisode, SimplifiedPlaylist,
Expand Down Expand Up @@ -60,3 +61,20 @@ pub enum SearchResult {
#[serde(rename = "episodes")]
Episodes(Page<SimplifiedEpisode>),
}

/// Valid filters to used in the search endpoint's query string
#[derive(Debug, Display, PartialEq, Eq, PartialOrd, Ord)]
#[strum(serialize_all = "snake_case")]
pub enum SearchFilter {
Album,
Artist,
Track,
Year,
Upc,
#[strum(serialize = "tag:hipster")]
TagHipster,
#[strum(serialize = "tag:new")]
TagNew,
Isrc,
Genre,
}
7 changes: 4 additions & 3 deletions src/clients/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -436,9 +436,9 @@ where
/// relevant audio content that is hosted externally.
///
/// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/search)
async fn search(
async fn search<T: Into<String> + Send>(
&self,
q: &str,
q: T,
_type: SearchType,
market: Option<Market>,
include_external: Option<IncludeExternal>,
Expand All @@ -447,8 +447,9 @@ where
) -> ClientResult<SearchResult> {
let limit = limit.map(|s| s.to_string());
let offset = offset.map(|s| s.to_string());
let q: String = q.into();
let params = build_map([
("q", Some(q)),
("q", Some(&q)),
("type", Some(_type.into())),
("market", market.map(Into::into)),
("include_external", include_external.map(Into::into)),
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ mod auth_code;
mod auth_code_pkce;
mod client_creds;
pub mod clients;
pub mod search;
pub mod sync;
mod util;

Expand Down
152 changes: 152 additions & 0 deletions src/search/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
use std::collections::BTreeMap;

use rspotify_model::SearchFilter;

/// Builder used to create search query.
///
/// Note that when calling the same function multiple times, the filter will be the text from the
/// last call
///
/// This is converted to the query string using into()
///
/// Exemple
/// ```rust
/// use rspotify::search::SearchQuery;
///
/// SearchQuery::default()
/// .any("foo")
/// .album("bar")
/// // Filter on album containing "bar" and anything containing "foo"
/// ```
///
/// For more information on the different filters, look at the [Soptify
/// documentation](https://developer.spotify.com/documentation/web-api/reference/#/operations/search)
#[derive(Debug, Default)]
pub struct SearchQuery<'a> {
no_filter_query: String,
query_map: BTreeMap<SearchFilter, &'a str>,
}
GartoxFR marked this conversation as resolved.
Show resolved Hide resolved

impl<'a> SearchQuery<'a> {
/// Basic filter where the given string can be anything
pub fn any(&mut self, str: &'a str) -> &mut Self {
self.no_filter_query.push_str(str);
self.no_filter_query.push(' ');
self
}

pub fn album(&mut self, str: &'a str) -> &mut Self {
self.query_map.insert(SearchFilter::Album, str);
self
}

pub fn artist(&mut self, str: &'a str) -> &mut Self {
self.query_map.insert(SearchFilter::Artist, str);
self
}

pub fn track(&mut self, str: &'a str) -> &mut Self {
self.query_map.insert(SearchFilter::Track, str);
self
}

pub fn year(&mut self, str: &'a str) -> &mut Self {
self.query_map.insert(SearchFilter::Year, str);
self
}

pub fn upc(&mut self, str: &'a str) -> &mut Self {
self.query_map.insert(SearchFilter::Upc, str);
self
}

pub fn tag_new(&mut self) -> &mut Self {
self.query_map.insert(SearchFilter::TagNew, "");
self
}

pub fn tag_hipster(&mut self) -> &mut Self {
self.query_map.insert(SearchFilter::TagHipster, "");
self
}

pub fn isrc(&mut self, str: &'a str) -> &mut Self {
self.query_map.insert(SearchFilter::Isrc, str);
self
}

pub fn genre(&mut self, str: &'a str) -> &mut Self {
self.query_map.insert(SearchFilter::Genre, str);
self
}
}

impl From<&SearchQuery<'_>> for String {
fn from(val: &SearchQuery) -> Self {
let mut rep = val.no_filter_query.clone();

if val.query_map.is_empty() {
return rep;
}

rep.push_str(
val.query_map
.iter()
.map(|entry| match entry.0 {
SearchFilter::TagNew | SearchFilter::TagHipster => format!("{} ", entry.0),
_ => format!("{}:{} ", entry.0, entry.1),
})
.collect::<String>()
.trim(),
);

rep
}
}

impl From<&mut SearchQuery<'_>> for String {
fn from(val: &mut SearchQuery) -> Self {
String::from(&(*val))
}
}

impl From<SearchQuery<'_>> for String {
fn from(val: SearchQuery) -> Self {
String::from(&val)
}
}

#[cfg(test)]
mod test {
use super::SearchQuery;

#[test]
fn test_search_query() {
let query: String = SearchQuery::default()
.any("foo")
.any("bar")
.album("wrong album")
.album("arrival")
.artist("abba")
.tag_new()
.tag_hipster()
.track("foo")
.year("2020")
.upc("bar")
.isrc("foo")
.genre("metal")
.into();

let expected = "foo bar album:arrival artist:abba track:foo year:2020 upc:bar \
tag:hipster tag:new isrc:foo genre:metal";

assert_eq!(expected, query);
}

#[test]
fn test_empty_query() {
let query: String = SearchQuery::default().into();

assert_eq!(query, "");
}
}
14 changes: 11 additions & 3 deletions tests/test_with_oauth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ use rspotify::{
SearchType, ShowId, TimeLimits, TimeRange, TrackId, UserId,
},
prelude::*,
scopes, AuthCodeSpotify, ClientResult, Credentials, OAuth, Token,
scopes,
search::SearchQuery,
AuthCodeSpotify, ClientResult, Credentials, OAuth, Token,
};

use std::env;
Expand Down Expand Up @@ -444,10 +446,16 @@ async fn test_repeat() {
#[maybe_async::test(feature = "__sync", async(feature = "__async", tokio::test))]
#[ignore]
async fn test_search_album() {
let query = "album:arrival artist:abba";
oauth_client()
.await
.search(query, SearchType::Album, None, None, Some(10), Some(0))
.search(
SearchQuery::default().album("arrival").artist("abba"),
SearchType::Album,
None,
None,
Some(10),
Some(0),
)
.await
.unwrap();
}
Expand Down