diff --git a/src/rust/amadeus-rs/amadeus-macro/src/lib.rs b/src/rust/amadeus-rs/amadeus-macro/src/lib.rs index 7e6a7e6107c33f1c24d0414d852f628a64e36443..3be4abf86039feae4d636763113449e2336d058e 100644 --- a/src/rust/amadeus-rs/amadeus-macro/src/lib.rs +++ b/src/rust/amadeus-rs/amadeus-macro/src/lib.rs @@ -1,5 +1,3 @@ -// https://doc.rust-lang.org/reference/macros-by-example.html - #[macro_export] macro_rules! either { ($test:expr => $true_expr:expr; $false_expr:expr) => { @@ -17,3 +15,14 @@ macro_rules! lkt_command_from_str { concat!($lit, '\n').to_owned() }; } + +#[macro_export] +macro_rules! then_some { + ($cond: expr => $some: expr) => { + if $cond { + Some($some) + } else { + None + } + }; +} diff --git a/src/rust/amadeus-rs/amadeus/src/utils/deamon.rs b/src/rust/amadeus-rs/amadeus/src/utils/deamon.rs index f7d312b3f251e366fd6dbb79a6184f27c990bb69..21317f21c7ef8abb5763d81ed0325b85eeacca83 100644 --- a/src/rust/amadeus-rs/amadeus/src/utils/deamon.rs +++ b/src/rust/amadeus-rs/amadeus/src/utils/deamon.rs @@ -153,14 +153,14 @@ impl Deamon for StatusDeamon { return_when_flagged!(quit_deamon, joined_deamon); let status = { - let mut res = match connexion.send_query(LektorQuery::PlaybackStatus) { + let res = match connexion.send_query(LektorQuery::PlaybackStatus) { Ok(res) => res, Err(e) => { error!("failed to send the playback status command to lektor: {e}"); continue; } }; - match LektorPlaybackStatusResponse::consume(&mut res) { + match LektorPlaybackStatusResponse::consume(res) { Err(e) => { error!("failed to build response from formated response: {e}"); continue; @@ -170,14 +170,14 @@ impl Deamon for StatusDeamon { }; let current = if status.state != LektorState::Stopped { - let mut res = match connexion.send_query(LektorQuery::CurrentKara) { + let res = match connexion.send_query(LektorQuery::CurrentKara) { Ok(res) => res, Err(e) => { error!("failed to send the current kara command to lektor: {e}",); continue; } }; - match LektorCurrentKaraResponse::consume(&mut res) { + match LektorCurrentKaraResponse::consume(res) { Ok(res) => Some(res), Err(err) => { error!("failed to build response from formated response: {err}"); diff --git a/src/rust/amadeus-rs/lkt-lib/src/connexion.rs b/src/rust/amadeus-rs/lkt-lib/src/connexion.rs index 8d88132e6bedbf9c3fb73be29628b4ced6d9a5de..4a3edbc85c2fa40685e340a723bf12635efbc8f8 100644 --- a/src/rust/amadeus-rs/lkt-lib/src/connexion.rs +++ b/src/rust/amadeus-rs/lkt-lib/src/connexion.rs @@ -13,6 +13,7 @@ use std::{ pub enum LektorQueryError { IO(io::Error), + Query(String), Other(String), } @@ -20,6 +21,7 @@ impl std::fmt::Display for LektorQueryError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { use LektorQueryError::*; match self { + Query(err) => write!(f, "lektor query logic error: {err}"), IO(io) => write!(f, "lektor query error io: {io}"), Other(other) => write!(f, "lektor query error: {other}"), } @@ -76,6 +78,7 @@ impl LektorConnexion { query: LektorQuery, ) -> Result<LektorFormatedResponse, LektorQueryError> { let mut res: Vec<String> = Vec::new(); + query.verify().map_err(LektorQueryError::Query)?; self.send_query_inner(query, &mut res) .map_err(LektorQueryError::IO)?; LektorFormatedResponse::try_from(res).map_err(LektorQueryError::Other) diff --git a/src/rust/amadeus-rs/lkt-lib/src/lib.rs b/src/rust/amadeus-rs/lkt-lib/src/lib.rs index 4d272fafa663d56974da0b460962db7373390b05..331d354dd87a2a3762e3e6686bab89c9f66dcaaf 100644 --- a/src/rust/amadeus-rs/lkt-lib/src/lib.rs +++ b/src/rust/amadeus-rs/lkt-lib/src/lib.rs @@ -3,7 +3,6 @@ mod connexion; mod constants; -mod macros; mod query; mod response; mod types; diff --git a/src/rust/amadeus-rs/lkt-lib/src/macros.rs b/src/rust/amadeus-rs/lkt-lib/src/macros.rs deleted file mode 100644 index 88ba1211cb5124c4d0849feb6a66257b53a4473c..0000000000000000000000000000000000000000 --- a/src/rust/amadeus-rs/lkt-lib/src/macros.rs +++ /dev/null @@ -1,10 +0,0 @@ -#[macro_export] -macro_rules! then_some { - ($cond: expr => $some: expr) => { - if $cond { - Some($some) - } else { - None - } - }; -} diff --git a/src/rust/amadeus-rs/lkt-lib/src/query.rs b/src/rust/amadeus-rs/lkt-lib/src/query.rs index 6bcf9b19d8847cc6377998482d322a09ea7e51bb..f0fe4dac1f6932d814857f68c5d10bdf020b70d7 100644 --- a/src/rust/amadeus-rs/lkt-lib/src/query.rs +++ b/src/rust/amadeus-rs/lkt-lib/src/query.rs @@ -2,7 +2,7 @@ use crate::uri::LektorUri; use amadeus_macro::lkt_command_from_str; -use std::string::ToString; +use std::{borrow::Borrow, string::ToString}; pub(crate) enum LektorQueryLineType { Ok, @@ -16,7 +16,7 @@ pub enum LektorQuery { Ping, Close, KillServer, - ConnectAsUser(String), + ConnectAsUser(String, Box<LektorQuery>), CurrentKara, PlaybackStatus, @@ -27,6 +27,7 @@ pub enum LektorQuery { ListAllPlaylists, ListPlaylist(String), + SearchKara(LektorUri), FindAddKara(LektorUri), InsertKara(LektorUri), @@ -76,6 +77,32 @@ impl LektorQuery { pub fn create_continuation(query: Self, cont: usize) -> Self { Self::Continuation(cont, Box::new(query)) } + + pub fn verify(&self) -> Result<(), String> { + use LektorQuery::*; + match self { + // User commands + SearchKara(_) | FindAddKara(_) | InsertKara(_) | AddKara(_) | PlaybackStatus + | PlayNext | PlayPrevious | ShuffleQueue | ListAllPlaylists | ListPlaylist(_) + | CurrentKara | Ping => Ok(()), + + // Should be admin commands + Close => Err("close is an admin command".to_string()), + KillServer => Err("kill server is an admin command".to_string()), + + // Admin commands + ConnectAsUser(_, cmd) => match cmd.borrow() { + Close | KillServer => Ok(()), + _ => Err(format!("not an admin command: {cmd:?}")), + }, + + // Continuation commands + Continuation(_, cmd) => match cmd.borrow() { + ListAllPlaylists | FindAddKara(_) | SearchKara(_) | ListPlaylist(_) => Ok(()), + _ => Err(format!("not a continuable command: {cmd:?}")), + }, + } + } } impl ToString for LektorQuery { @@ -85,7 +112,16 @@ impl ToString for LektorQuery { Ping => lkt_command_from_str!("ping"), Close => lkt_command_from_str!("close"), KillServer => lkt_command_from_str!("kill"), - ConnectAsUser(password) => format!("password {}\n", password), + ConnectAsUser(password, cmd) => format!( + concat!( + "command_list_begin\n", + "password {}\n", + "{}\n", + "command_list_end\n", + ), + password, + cmd.to_string() + ), CurrentKara => lkt_command_from_str!("currentsong"), PlaybackStatus => lkt_command_from_str!("status"), @@ -96,6 +132,7 @@ impl ToString for LektorQuery { ListAllPlaylists => lkt_command_from_str!("listplaylists"), ListPlaylist(plt_name) => format!("listplaylist {}\n", plt_name), + SearchKara(uri) => format!("find {}\n", uri.to_string()), FindAddKara(uri) => format!("findadd {}\n", uri.to_string()), InsertKara(uri) => format!("__insert {}\n", uri.to_string()), diff --git a/src/rust/amadeus-rs/lkt-lib/src/response.rs b/src/rust/amadeus-rs/lkt-lib/src/response.rs index 585ebdfa45f94980aff71b284c82f4378a246b1f..d80e662f86cc8e10743db422235e5c08a08f19cc 100644 --- a/src/rust/amadeus-rs/lkt-lib/src/response.rs +++ b/src/rust/amadeus-rs/lkt-lib/src/response.rs @@ -1,14 +1,19 @@ //! Contains types for typed response. -use crate::then_some; use crate::types::LektorState; +use amadeus_macro::*; +/// A formated response is just a list of key/pairs. We get every line that is +/// not Ok/Ack/Continue (i.e. data lines) and split on the first ':' and trim +/// spaces from the keys and the values. The keys are always in lowercase. #[derive(Debug)] pub struct LektorFormatedResponse { content: Vec<(String, String)>, } impl LektorFormatedResponse { + /// Pop the first key found in the response, get an error if the key is not + /// found. If multiple keys are found, only the first found is returned. pub fn pop(&mut self, key: &str) -> Result<String, String> { match self .content @@ -20,6 +25,23 @@ impl LektorFormatedResponse { None => Err(format!("no key {key} was found in formated response")), } } + + /// Pop all the entries with the said key. If no key is found then the empty + /// vector is returned. This function can't fail. + /// + /// FIXME: Get ride of the clone in this function... + pub fn pop_all(&mut self, key: &str) -> Vec<String> { + let mut ret = Vec::new(); + self.content.retain(|(what, field)| { + if *what == key { + ret.push(field.clone()); + false + } else { + true + } + }); + ret + } } impl IntoIterator for LektorFormatedResponse { @@ -54,6 +76,25 @@ impl TryFrom<Vec<String>> for LektorFormatedResponse { pub enum LektordResponse { PlaybackStatus(LektorPlaybackStatusResponse), CurrentKara(LektorCurrentKaraResponse), + PlaylistList(LektorPlaylistListResponse), +} + +/// A trait for typed lektor responses. Such responses must be built by +/// consuming a formated response. We also protect from implemeting this trait +/// outside of this crate. +pub trait FromLektorResponse: Sized + std::fmt::Debug + private::Sealed { + /// Consume a formated response to produce the correctly typed response. May + /// got an error as a string that describes the problem. + fn consume(response: LektorFormatedResponse) -> Result<Self, String>; +} + +mod private { + use super::*; + pub trait Sealed {} + impl Sealed for LektorPlaybackStatusResponse {} + impl Sealed for LektorCurrentKaraResponse {} + impl Sealed for LektorPlaylistListResponse {} + impl Sealed for LektorEmptyResponse {} } #[derive(Debug)] @@ -72,8 +113,59 @@ pub struct LektorPlaybackStatusResponse { pub repeat: bool, } -impl LektorPlaybackStatusResponse { - pub fn consume(response: &mut LektorFormatedResponse) -> Result<Self, String> { +#[derive(Debug)] +pub struct LektorPlaylistListResponse { + pub playlists: Vec<String>, +} + +#[derive(Debug)] +pub struct LektorCurrentKaraInnerResponse { + pub title: String, + pub author: String, + pub source: String, + pub song_type: String, + pub song_number: Option<usize>, + pub category: String, + pub language: String, +} + +#[derive(Debug)] +pub struct LektorCurrentKaraResponse { + pub content: Option<LektorCurrentKaraInnerResponse>, +} + +#[derive(Debug)] +pub struct LektorEmptyResponse; + +impl LektorCurrentKaraInnerResponse { + /// If the response is partial we might want to return none (if we are not + /// playing anything for example...) + pub(self) fn is_partial(&self) -> bool { + self.title.is_empty() + || self.author.is_empty() + || self.source.is_empty() + || self.song_type.is_empty() + || self.category.is_empty() + || self.language.is_empty() + } +} + +impl FromLektorResponse for LektorEmptyResponse { + fn consume(_: LektorFormatedResponse) -> Result<Self, String> { + Ok(Self {}) + } +} + +impl FromLektorResponse for LektorPlaylistListResponse { + fn consume(mut response: LektorFormatedResponse) -> Result<Self, String> { + Ok(Self { + playlists: response.pop_all("name"), + }) + } +} + +impl FromLektorResponse for LektorPlaybackStatusResponse { + fn consume(mut response: LektorFormatedResponse) -> Result<Self, String> { let mut ret = Self { elapsed: response.pop("elapsed")?.parse::<usize>().unwrap_or(0), songid: match response.pop("songid")?.parse::<isize>() { @@ -108,19 +200,8 @@ impl LektorPlaybackStatusResponse { } } -#[derive(Debug)] -pub struct LektorCurrentKaraResponse { - pub title: String, - pub author: String, - pub source: String, - pub song_type: String, - pub song_number: Option<usize>, - pub category: String, - pub language: String, -} - -impl LektorCurrentKaraResponse { - pub fn consume(response: &mut LektorFormatedResponse) -> Result<Self, String> { +impl FromLektorResponse for LektorCurrentKaraResponse { + fn consume(mut response: LektorFormatedResponse) -> Result<Self, String> { let song_type_number = response.pop("type")?; let (song_type, song_number) = match song_type_number.find(char::is_numeric) { Some(index) => ( @@ -133,7 +214,7 @@ impl LektorCurrentKaraResponse { None => panic!("Oupsy"), }; - Ok(Self { + let inner = LektorCurrentKaraInnerResponse { title: response.pop("title")?, author: response.pop("author")?, source: response.pop("source")?, @@ -141,6 +222,10 @@ impl LektorCurrentKaraResponse { language: response.pop("language")?, song_type, song_number, + }; + + Ok(Self { + content: then_some!(!inner.is_partial() => inner), }) } } diff --git a/src/rust/amadeus-rs/lkt-rs/src/main.rs b/src/rust/amadeus-rs/lkt-rs/src/main.rs index 01aea5fe5c8f1bb629914ac2f58f9e8a24b8863d..f7d6f81f7fd9f086c930e47ba414f1351b9e83cd 100644 --- a/src/rust/amadeus-rs/lkt-rs/src/main.rs +++ b/src/rust/amadeus-rs/lkt-rs/src/main.rs @@ -4,19 +4,18 @@ fn main() { let mut lektor = LektorConnexion::new("localhost".to_owned(), 6600).unwrap(); if lektor.send_query(LektorQuery::Ping).is_ok() {} - if let Ok(mut response) = lektor.send_query(LektorQuery::CurrentKara) { - let current_kara = LektorCurrentKaraResponse::consume(&mut response); + if let Ok(response) = lektor.send_query(LektorQuery::CurrentKara) { + let current_kara = LektorCurrentKaraResponse::consume(response); println!("CURRENT {:?}", current_kara); } - if let Ok(mut response) = lektor.send_query(LektorQuery::PlaybackStatus) { - let playback_status = LektorPlaybackStatusResponse::consume(&mut response); + if let Ok(response) = lektor.send_query(LektorQuery::PlaybackStatus) { + let playback_status = LektorPlaybackStatusResponse::consume(response); println!("PLAYBACK-STATUS {:?}", playback_status); } if let Ok(response) = lektor.send_query(LektorQuery::ListAllPlaylists) { - for (what, item) in response.into_iter() { - println!("ALL PLAYLISTS {what}:{item}"); - } + let plts_response = LektorPlaylistListResponse::consume(response); + println!("ALL-PLTS {:?}", plts_response); } }