Skip to content
Extraits de code Groupes Projets
Vérifiée Valider 7987bc45 rédigé par Kubat's avatar Kubat
Parcourir les fichiers

AMADEUS: Check queries before sending them + implement more typed responses


Signed-off-by: default avatarKubat <mael.martin31@gmail.com>
parent fdbdc8e3
Aucune branche associée trouvée
Aucune étiquette associée trouvée
1 requête de fusion!193AMADEUS: Implementation of lkt-lib
Pipeline #3230 en échec
// 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
}
};
}
......@@ -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}");
......
......@@ -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)
......
......@@ -3,7 +3,6 @@
mod connexion;
mod constants;
mod macros;
mod query;
mod response;
mod types;
......
#[macro_export]
macro_rules! then_some {
($cond: expr => $some: expr) => {
if $cond {
Some($some)
} else {
None
}
};
}
......@@ -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()),
......
//! 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),
})
}
}
......@@ -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);
}
}
0% Chargement en cours ou .
You are about to add 0 people to the discussion. Proceed with caution.
Terminez d'abord l'édition de ce message.
Veuillez vous inscrire ou vous pour commenter