From caf6feea7ab6dc8e44d422ce193f0c730216227a Mon Sep 17 00:00:00 2001 From: Antoine Gersant <antoine.gersant@lesforges.org> Date: Sat, 27 Jul 2024 18:47:32 -0700 Subject: [PATCH] API versioning tests --- src/server.rs | 22 ++++++++++ src/server/axum/api.rs | 81 ++++++++++++++++++++--------------- src/server/axum/version.rs | 19 +++----- src/server/dto.rs | 3 -- src/server/test.rs | 5 ++- src/server/test/admin.rs | 3 +- src/server/test/auth.rs | 9 ++-- src/server/test/collection.rs | 79 ++++++++++++++++++++++++++-------- src/server/test/playlist.rs | 7 +-- src/server/test/protocol.rs | 37 +++++++++++++--- 10 files changed, 179 insertions(+), 86 deletions(-) diff --git a/src/server.rs b/src/server.rs index 703ab37..5e21bd1 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,8 +1,30 @@ +use error::APIError; + mod dto; mod error; #[cfg(test)] mod test; +pub enum APIMajorVersion { + V7, + V8, +} + +impl TryFrom<i32> for APIMajorVersion { + type Error = APIError; + + fn try_from(value: i32) -> Result<Self, Self::Error> { + match value { + 7 => Ok(Self::V7), + 8 => Ok(Self::V8), + _ => Err(APIError::UnsupportedAPIVersion), + } + } +} + +pub const API_MAJOR_VERSION: i32 = 8; +pub const API_MINOR_VERSION: i32 = 0; + mod axum; pub use axum::*; diff --git a/src/server/axum/api.rs b/src/server/axum/api.rs index c6ab3e3..343d04b 100644 --- a/src/server/axum/api.rs +++ b/src/server/axum/api.rs @@ -12,11 +12,10 @@ use percent_encoding::percent_decode_str; use crate::{ app::{config, ddns, index, lastfm, playlist, scanner, settings, thumbnail, user, vfs, App}, - server::{dto, error::APIError}, + server::{dto, error::APIError, APIMajorVersion, API_MAJOR_VERSION, API_MINOR_VERSION}, }; use super::auth::{AdminRights, Auth}; -use super::version::Version; pub fn router() -> Router<App> { Router::new() @@ -68,8 +67,8 @@ pub fn router() -> Router<App> { async fn get_version() -> Json<dto::Version> { let current_version = dto::Version { - major: dto::API_MAJOR_VERSION, - minor: dto::API_MINOR_VERSION, + major: API_MAJOR_VERSION, + minor: API_MINOR_VERSION, }; Json(current_version) } @@ -254,16 +253,19 @@ async fn post_trigger_index( Ok(()) } -fn collection_files_to_response(files: Vec<index::CollectionFile>, version: Version) -> Response { - match version { - Version::V7 => Json( +fn collection_files_to_response( + files: Vec<index::CollectionFile>, + api_version: APIMajorVersion, +) -> Response { + match api_version { + APIMajorVersion::V7 => Json( files .into_iter() .map(|f| f.into()) .collect::<Vec<dto::v7::CollectionFile>>(), ) .into_response(), - Version::V8 => Json( + APIMajorVersion::V8 => Json( files .into_iter() .map(|f| f.into()) @@ -273,16 +275,16 @@ fn collection_files_to_response(files: Vec<index::CollectionFile>, version: Vers } } -fn songs_to_response(files: Vec<scanner::Song>, version: Version) -> Response { - match version { - Version::V7 => Json( +fn songs_to_response(files: Vec<scanner::Song>, api_version: APIMajorVersion) -> Response { + match api_version { + APIMajorVersion::V7 => Json( files .into_iter() .map(|f| f.into()) .collect::<Vec<dto::v7::Song>>(), ) .into_response(), - Version::V8 => Json( + APIMajorVersion::V8 => Json( files .into_iter() .map(|f| f.into()) @@ -292,16 +294,19 @@ fn songs_to_response(files: Vec<scanner::Song>, version: Version) -> Response { } } -fn directories_to_response(files: Vec<scanner::Directory>, version: Version) -> Response { - match version { - Version::V7 => Json( +fn directories_to_response( + files: Vec<scanner::Directory>, + api_version: APIMajorVersion, +) -> Response { + match api_version { + APIMajorVersion::V7 => Json( files .into_iter() .map(|f| f.into()) .collect::<Vec<dto::v7::Directory>>(), ) .into_response(), - Version::V8 => Json( + APIMajorVersion::V8 => Json( files .into_iter() .map(|f| f.into()) @@ -313,19 +318,19 @@ fn directories_to_response(files: Vec<scanner::Directory>, version: Version) -> async fn get_browse_root( _auth: Auth, - version: Version, + api_version: APIMajorVersion, State(index): State<index::Index>, ) -> Response { let result = match index.browse(std::path::Path::new("")).await { Ok(r) => r, Err(e) => return APIError::from(e).into_response(), }; - collection_files_to_response(result, version) + collection_files_to_response(result, api_version) } async fn get_browse( _auth: Auth, - version: Version, + api_version: APIMajorVersion, State(index): State<index::Index>, Path(path): Path<String>, ) -> Response { @@ -334,24 +339,24 @@ async fn get_browse( Ok(r) => r, Err(e) => return APIError::from(e).into_response(), }; - collection_files_to_response(result, version) + collection_files_to_response(result, api_version) } async fn get_flatten_root( _auth: Auth, - version: Version, + api_version: APIMajorVersion, State(index): State<index::Index>, ) -> Response { let songs = match index.flatten(std::path::Path::new("")).await { Ok(s) => s, Err(e) => return APIError::from(e).into_response(), }; - songs_to_response(songs, version) + songs_to_response(songs, api_version) } async fn get_flatten( _auth: Auth, - version: Version, + api_version: APIMajorVersion, State(index): State<index::Index>, Path(path): Path<String>, ) -> Response { @@ -360,40 +365,48 @@ async fn get_flatten( Ok(s) => s, Err(e) => return APIError::from(e).into_response(), }; - songs_to_response(songs, version) + songs_to_response(songs, api_version) } -async fn get_random(_auth: Auth, version: Version, State(index): State<index::Index>) -> Response { +async fn get_random( + _auth: Auth, + api_version: APIMajorVersion, + State(index): State<index::Index>, +) -> Response { let directories = match index.get_random_albums(20).await { Ok(d) => d, Err(e) => return APIError::from(e).into_response(), }; - directories_to_response(directories, version) + directories_to_response(directories, api_version) } -async fn get_recent(_auth: Auth, version: Version, State(index): State<index::Index>) -> Response { +async fn get_recent( + _auth: Auth, + api_version: APIMajorVersion, + State(index): State<index::Index>, +) -> Response { let directories = match index.get_recent_albums(20).await { Ok(d) => d, Err(e) => return APIError::from(e).into_response(), }; - directories_to_response(directories, version) + directories_to_response(directories, api_version) } async fn get_search_root( _auth: Auth, - version: Version, + api_version: APIMajorVersion, State(index): State<index::Index>, ) -> Response { let files = match index.search("").await { Ok(f) => f, Err(e) => return APIError::from(e).into_response(), }; - collection_files_to_response(files, version) + collection_files_to_response(files, api_version) } async fn get_search( _auth: Auth, - version: Version, + api_version: APIMajorVersion, State(index): State<index::Index>, Path(query): Path<String>, ) -> Response { @@ -401,7 +414,7 @@ async fn get_search( Ok(f) => f, Err(e) => return APIError::from(e).into_response(), }; - collection_files_to_response(files, version) + collection_files_to_response(files, api_version) } async fn get_playlists( @@ -431,7 +444,7 @@ async fn put_playlist( async fn get_playlist( auth: Auth, - version: Version, + api_version: APIMajorVersion, State(playlist_manager): State<playlist::Manager>, Path(name): Path<String>, ) -> Response { @@ -442,7 +455,7 @@ async fn get_playlist( Ok(s) => s, Err(e) => return APIError::from(e).into_response(), }; - songs_to_response(songs, version) + songs_to_response(songs, api_version) } async fn delete_playlist( diff --git a/src/server/axum/version.rs b/src/server/axum/version.rs index db711f7..67e8791 100644 --- a/src/server/axum/version.rs +++ b/src/server/axum/version.rs @@ -1,15 +1,10 @@ use axum::{async_trait, extract::FromRequestParts}; use http::request::Parts; -use crate::server::{dto, error::APIError}; - -pub enum Version { - V7, - V8, -} +use crate::server::{error::APIError, APIMajorVersion}; #[async_trait] -impl<S> FromRequestParts<S> for Version +impl<S> FromRequestParts<S> for APIMajorVersion where S: Send + Sync, { @@ -19,18 +14,14 @@ where let version_header = match parts.headers.get("Accept-Version").map(|h| h.to_str()) { Some(Ok(h)) => h, Some(Err(_)) => return Err(APIError::InvalidAPIVersionHeader), - None => return Ok(Version::V7), // TODO Drop support for implicit version in future release + None => return Ok(APIMajorVersion::V7), // TODO Drop support for implicit version in future release }; - let version: dto::Version = match serde_json::from_str(version_header) { + let version = match str::parse::<i32>(version_header) { Ok(v) => v, Err(_) => return Err(APIError::APIVersionHeaderParseError), }; - Ok(match version.major { - 7 => Version::V7, - 8 => Version::V8, - _ => return Err(APIError::UnsupportedAPIVersion), - }) + APIMajorVersion::try_from(version) } } diff --git a/src/server/dto.rs b/src/server/dto.rs index 80386be..cf0bc61 100644 --- a/src/server/dto.rs +++ b/src/server/dto.rs @@ -1,7 +1,4 @@ pub mod v7; pub mod v8; -pub const API_MAJOR_VERSION: i32 = 8; -pub const API_MINOR_VERSION: i32 = 0; - pub use v8::*; diff --git a/src/server/test.rs b/src/server/test.rs index 9c483a9..ea0857f 100644 --- a/src/server/test.rs +++ b/src/server/test.rs @@ -24,6 +24,7 @@ mod web; use crate::server::dto; use crate::server::test::constants::*; +use protocol::V8; pub use crate::server::axum::test::ServiceType; @@ -116,7 +117,7 @@ pub trait TestService { assert_eq!(response.status(), StatusCode::OK); loop { - let browse_request = protocol::browse(Path::new("")); + let browse_request = protocol::browse::<V8>(Path::new("")); let response = self .fetch_json::<(), Vec<dto::CollectionFile>>(&browse_request) .await; @@ -128,7 +129,7 @@ pub trait TestService { } loop { - let flatten_request = protocol::flatten(Path::new("")); + let flatten_request = protocol::flatten::<V8>(Path::new("")); let response = self.fetch_json::<_, Vec<dto::Song>>(&flatten_request).await; let entries = response.body(); if !entries.is_empty() { diff --git a/src/server/test/admin.rs b/src/server/test/admin.rs index 0cefc85..4497c24 100644 --- a/src/server/test/admin.rs +++ b/src/server/test/admin.rs @@ -1,6 +1,7 @@ use http::StatusCode; use crate::server::dto; +use crate::server::test::protocol::V8; use crate::server::test::{protocol, ServiceType, TestService}; use crate::test_name; @@ -47,7 +48,7 @@ async fn trigger_index_golden_path() { service.complete_initial_setup().await; service.login_admin().await; - let request = protocol::random(); + let request = protocol::random::<V8>(); let response = service.fetch_json::<_, Vec<dto::Directory>>(&request).await; let entries = response.body(); diff --git a/src/server/test/auth.rs b/src/server/test/auth.rs index 242c018..c6c4ba7 100644 --- a/src/server/test/auth.rs +++ b/src/server/test/auth.rs @@ -2,6 +2,7 @@ use headers::{self, HeaderMapExt}; use http::StatusCode; use crate::server::dto; +use crate::server::test::protocol::V8; use crate::server::test::{constants::*, protocol, ServiceType, TestService}; use crate::test_name; @@ -45,7 +46,7 @@ async fn authentication_via_bearer_http_header_rejects_bad_token() { let mut service = ServiceType::new(&test_name!()).await; service.complete_initial_setup().await; - let mut request = protocol::random(); + let mut request = protocol::random::<V8>(); let bearer = headers::Authorization::bearer("garbage").unwrap(); request.headers_mut().typed_insert(bearer); @@ -67,7 +68,7 @@ async fn authentication_via_bearer_http_header_golden_path() { service.logout().await; - let mut request = protocol::random(); + let mut request = protocol::random::<V8>(); let bearer = headers::Authorization::bearer(&authorization.token).unwrap(); request.headers_mut().typed_insert(bearer); let response = service.fetch(&request).await; @@ -79,7 +80,7 @@ async fn authentication_via_query_param_rejects_bad_token() { let mut service = ServiceType::new(&test_name!()).await; service.complete_initial_setup().await; - let mut request = protocol::random(); + let mut request = protocol::random::<V8>(); *request.uri_mut() = (request.uri().to_string() + "?auth_token=garbage-token") .parse() .unwrap(); @@ -102,7 +103,7 @@ async fn authentication_via_query_param_golden_path() { service.logout().await; - let mut request = protocol::random(); + let mut request = protocol::random::<V8>(); *request.uri_mut() = format!("{}?auth_token={}", request.uri(), authorization.token) .parse() .unwrap(); diff --git a/src/server/test/collection.rs b/src/server/test/collection.rs index e236724..41ca32b 100644 --- a/src/server/test/collection.rs +++ b/src/server/test/collection.rs @@ -2,13 +2,14 @@ use http::StatusCode; use std::path::{Path, PathBuf}; use crate::server::dto; +use crate::server::test::protocol::{V7, V8}; use crate::server::test::{add_trailing_slash, constants::*, protocol, ServiceType, TestService}; use crate::test_name; #[tokio::test] async fn browse_requires_auth() { let mut service = ServiceType::new(&test_name!()).await; - let request = protocol::browse(&PathBuf::new()); + let request = protocol::browse::<V8>(&PathBuf::new()); let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } @@ -21,7 +22,7 @@ async fn browse_root() { service.index().await; service.login().await; - let request = protocol::browse(&PathBuf::new()); + let request = protocol::browse::<V8>(&PathBuf::new()); let response = service .fetch_json::<_, Vec<dto::CollectionFile>>(&request) .await; @@ -39,7 +40,7 @@ async fn browse_directory() { service.login().await; let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted"].iter().collect(); - let request = protocol::browse(&path); + let request = protocol::browse::<V8>(&path); let response = service .fetch_json::<_, Vec<dto::CollectionFile>>(&request) .await; @@ -49,21 +50,43 @@ async fn browse_directory() { } #[tokio::test] -async fn browse_bad_directory() { +async fn browse_missing_directory() { let mut service = ServiceType::new(&test_name!()).await; service.complete_initial_setup().await; service.login().await; let path: PathBuf = ["not_my_collection"].iter().collect(); - let request = protocol::browse(&path); + let request = protocol::browse::<V8>(&path); let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::NOT_FOUND); } +#[tokio::test] +async fn browse_directory_api_v7() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login_admin().await; + service.index().await; + service.login().await; + + let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted"].iter().collect(); + let request = protocol::browse::<V7>(&path); + let response = service + .fetch_json::<_, Vec<dto::v7::CollectionFile>>(&request) + .await; + assert_eq!(response.status(), StatusCode::OK); + let entries = response.body(); + assert_eq!(entries.len(), 5); + match &entries[0] { + dto::v7::CollectionFile::Song(s) => assert_eq!(s.artist.as_deref(), Some("Khemmis")), + _ => (), + } +} + #[tokio::test] async fn flatten_requires_auth() { let mut service = ServiceType::new(&test_name!()).await; - let request = protocol::flatten(&PathBuf::new()); + let request = protocol::flatten::<V8>(&PathBuf::new()); let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } @@ -76,7 +99,7 @@ async fn flatten_root() { service.index().await; service.login().await; - let request = protocol::flatten(&PathBuf::new()); + let request = protocol::flatten::<V8>(&PathBuf::new()); let response = service.fetch_json::<_, Vec<dto::Song>>(&request).await; assert_eq!(response.status(), StatusCode::OK); let entries = response.body(); @@ -91,7 +114,7 @@ async fn flatten_directory() { service.index().await; service.login().await; - let request = protocol::flatten(Path::new(TEST_MOUNT_NAME)); + let request = protocol::flatten::<V8>(Path::new(TEST_MOUNT_NAME)); let response = service.fetch_json::<_, Vec<dto::Song>>(&request).await; assert_eq!(response.status(), StatusCode::OK); let entries = response.body(); @@ -99,21 +122,39 @@ async fn flatten_directory() { } #[tokio::test] -async fn flatten_bad_directory() { +async fn flatten_missing_directory() { let mut service = ServiceType::new(&test_name!()).await; service.complete_initial_setup().await; service.login().await; let path: PathBuf = ["not_my_collection"].iter().collect(); - let request = protocol::flatten(&path); + let request = protocol::flatten::<V8>(&path); let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::NOT_FOUND); } +#[tokio::test] +async fn flatten_directory_api_v7() { + let mut service = ServiceType::new(&test_name!()).await; + service.complete_initial_setup().await; + service.login_admin().await; + service.index().await; + service.login().await; + + let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted"].iter().collect(); + let request = protocol::flatten::<V7>(&path); + let response = service.fetch_json::<_, Vec<dto::v7::Song>>(&request).await; + assert_eq!(response.status(), StatusCode::OK); + let entries = response.body(); + assert_eq!(entries.len(), 5); + + assert_eq!(entries[0].artist.as_deref(), Some("Khemmis")); +} + #[tokio::test] async fn random_requires_auth() { let mut service = ServiceType::new(&test_name!()).await; - let request = protocol::random(); + let request = protocol::random::<V8>(); let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } @@ -126,7 +167,7 @@ async fn random_golden_path() { service.index().await; service.login().await; - let request = protocol::random(); + let request = protocol::random::<V8>(); let response = service.fetch_json::<_, Vec<dto::Directory>>(&request).await; assert_eq!(response.status(), StatusCode::OK); let entries = response.body(); @@ -141,7 +182,7 @@ async fn random_with_trailing_slash() { service.index().await; service.login().await; - let mut request = protocol::random(); + let mut request = protocol::random::<V8>(); add_trailing_slash(&mut request); let response = service.fetch_json::<_, Vec<dto::Directory>>(&request).await; assert_eq!(response.status(), StatusCode::OK); @@ -152,7 +193,7 @@ async fn random_with_trailing_slash() { #[tokio::test] async fn recent_requires_auth() { let mut service = ServiceType::new(&test_name!()).await; - let request = protocol::recent(); + let request = protocol::recent::<V8>(); let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } @@ -165,7 +206,7 @@ async fn recent_golden_path() { service.index().await; service.login().await; - let request = protocol::recent(); + let request = protocol::recent::<V8>(); let response = service.fetch_json::<_, Vec<dto::Directory>>(&request).await; assert_eq!(response.status(), StatusCode::OK); let entries = response.body(); @@ -180,7 +221,7 @@ async fn recent_with_trailing_slash() { service.index().await; service.login().await; - let mut request = protocol::recent(); + let mut request = protocol::recent::<V8>(); add_trailing_slash(&mut request); let response = service.fetch_json::<_, Vec<dto::Directory>>(&request).await; assert_eq!(response.status(), StatusCode::OK); @@ -191,7 +232,7 @@ async fn recent_with_trailing_slash() { #[tokio::test] async fn search_requires_auth() { let mut service = ServiceType::new(&test_name!()).await; - let request = protocol::search(""); + let request = protocol::search::<V8>(""); let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } @@ -202,7 +243,7 @@ async fn search_without_query() { service.complete_initial_setup().await; service.login().await; - let request = protocol::search(""); + let request = protocol::search::<V8>(""); let response = service .fetch_json::<_, Vec<dto::CollectionFile>>(&request) .await; @@ -217,7 +258,7 @@ async fn search_with_query() { service.index().await; service.login().await; - let request = protocol::search("door"); + let request = protocol::search::<V8>("door"); let response = service .fetch_json::<_, Vec<dto::CollectionFile>>(&request) .await; diff --git a/src/server/test/playlist.rs b/src/server/test/playlist.rs index 4ba8205..c2ee982 100644 --- a/src/server/test/playlist.rs +++ b/src/server/test/playlist.rs @@ -1,6 +1,7 @@ use http::StatusCode; use crate::server::dto; +use crate::server::test::protocol::V8; use crate::server::test::{constants::*, protocol, ServiceType, TestService}; use crate::test_name; @@ -63,7 +64,7 @@ async fn save_playlist_large() { #[tokio::test] async fn get_playlist_requires_auth() { let mut service = ServiceType::new(&test_name!()).await; - let request = protocol::read_playlist(TEST_PLAYLIST_NAME); + let request = protocol::read_playlist::<V8>(TEST_PLAYLIST_NAME); let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } @@ -81,7 +82,7 @@ async fn get_playlist_golden_path() { assert_eq!(response.status(), StatusCode::OK); } - let request = protocol::read_playlist(TEST_PLAYLIST_NAME); + let request = protocol::read_playlist::<V8>(TEST_PLAYLIST_NAME); let response = service.fetch_json::<_, Vec<dto::Song>>(&request).await; assert_eq!(response.status(), StatusCode::OK); } @@ -92,7 +93,7 @@ async fn get_playlist_bad_name_returns_not_found() { service.complete_initial_setup().await; service.login().await; - let request = protocol::read_playlist(TEST_PLAYLIST_NAME); + let request = protocol::read_playlist::<V8>(TEST_PLAYLIST_NAME); let response = service.fetch(&request).await; assert_eq!(response.status(), StatusCode::NOT_FOUND); } diff --git a/src/server/test/protocol.rs b/src/server/test/protocol.rs index b28f106..b452af2 100644 --- a/src/server/test/protocol.rs +++ b/src/server/test/protocol.rs @@ -5,6 +5,25 @@ use std::path::Path; use crate::server::dto; use crate::{app::user, server::dto::ThumbnailSize}; +pub trait ProtocolVersion { + fn header_value() -> i32; +} + +pub struct V7; +pub struct V8; + +impl ProtocolVersion for V7 { + fn header_value() -> i32 { + 7 + } +} + +impl ProtocolVersion for V8 { + fn header_value() -> i32 { + 8 + } +} + pub fn web_index() -> Request<()> { Request::builder() .method(Method::GET) @@ -145,45 +164,50 @@ pub fn trigger_index() -> Request<()> { .unwrap() } -pub fn browse(path: &Path) -> Request<()> { +pub fn browse<VERSION: ProtocolVersion>(path: &Path) -> Request<()> { let path = path.to_string_lossy(); let endpoint = format!("/api/browse/{}", url_encode(path.as_ref())); Request::builder() + .header("Accept-Version", VERSION::header_value()) .method(Method::GET) .uri(&endpoint) .body(()) .unwrap() } -pub fn flatten(path: &Path) -> Request<()> { +pub fn flatten<VERSION: ProtocolVersion>(path: &Path) -> Request<()> { let path = path.to_string_lossy(); let endpoint = format!("/api/flatten/{}", url_encode(path.as_ref())); Request::builder() + .header("Accept-Version", VERSION::header_value()) .method(Method::GET) .uri(&endpoint) .body(()) .unwrap() } -pub fn random() -> Request<()> { +pub fn random<VERSION: ProtocolVersion>() -> Request<()> { Request::builder() + .header("Accept-Version", VERSION::header_value()) .method(Method::GET) .uri("/api/random") .body(()) .unwrap() } -pub fn recent() -> Request<()> { +pub fn recent<VERSION: ProtocolVersion>() -> Request<()> { Request::builder() + .header("Accept-Version", VERSION::header_value()) .method(Method::GET) .uri("/api/recent") .body(()) .unwrap() } -pub fn search(query: &str) -> Request<()> { +pub fn search<VERSION: ProtocolVersion>(query: &str) -> Request<()> { let endpoint = format!("/api/search/{}", url_encode(query)); Request::builder() + .header("Accept-Version", VERSION::header_value()) .method(Method::GET) .uri(&endpoint) .body(()) @@ -253,9 +277,10 @@ pub fn save_playlist( .unwrap() } -pub fn read_playlist(name: &str) -> Request<()> { +pub fn read_playlist<VERSION: ProtocolVersion>(name: &str) -> Request<()> { let endpoint = format!("/api/playlist/{}", url_encode(name)); Request::builder() + .header("Accept-Version", VERSION::header_value()) .method(Method::GET) .uri(&endpoint) .body(())