use crate::types::{ audio_player::VolumeTarget, commands::*, config::DaemonConfig, socket::Request, }; use std::path::PathBuf; /// Which of the two output paths a `*_monitoring_volume` / `*_mic_volume` command means. fn volume_target(command_name: &str) -> Option { match command_name { "get_monitoring_volume" | "set_monitoring_volume" => Some(VolumeTarget::Monitoring), "get_mic_volume" | "set_mic_volume" => Some(VolumeTarget::Mic), _ => None, } } pub fn parse_command(request: &Request) -> Option> { let id = request.args.get("id").and_then(|s| s.parse::().ok()); match request.name.as_str() { "ping" => Some(Box::new(PingCommand {})), "kill" => Some(Box::new(KillCommand {})), "pause" => Some(Box::new(PauseCommand { id })), "resume" => Some(Box::new(ResumeCommand { id })), "toggle_pause" => Some(Box::new(TogglePauseCommand { id })), "stop" => Some(Box::new(StopCommand { id })), "is_paused" => Some(Box::new(IsPausedCommand {})), "get_state" => Some(Box::new(GetStateCommand {})), "get_volume" => Some(Box::new(GetVolumeCommand { id })), "get_volume_multiplier" => Some(Box::new(GetVolumeMultiplierCommand {})), "set_volume" => { let volume = request .args .get("volume") .unwrap_or(&String::new()) .parse::() .ok(); Some(Box::new(SetVolumeCommand { volume, id })) } "get_monitoring_volume" | "get_mic_volume" => Some(Box::new(GetMasterVolumeCommand { target: volume_target(&request.name)?, })), "set_monitoring_volume" | "set_mic_volume" => { let volume = request .args .get("volume") .unwrap_or(&String::new()) .parse::() .ok(); Some(Box::new(SetMasterVolumeCommand { volume, target: volume_target(&request.name)?, })) } "set_volume_multiplier" => { let volume_multiplier = request .args .get("volume_multiplier") .unwrap_or(&String::new()) .parse::() .ok(); Some(Box::new(SetVolumeMultiplierCommand { volume_multiplier })) } "get_position" => Some(Box::new(GetPositionCommand { id })), "seek" => { let position = request .args .get("position") .unwrap_or(&String::new()) .parse::() .ok(); Some(Box::new(SeekCommand { position, id })) } "get_duration" => Some(Box::new(GetDurationCommand { id })), "play" => { let file_path = request .args .get("file_path") .unwrap_or(&String::new()) .parse::() .ok(); let concurrent = request .args .get("concurrent") .unwrap_or(&String::new()) .parse::() .ok(); Some(Box::new(PlayCommand { file_path, concurrent, })) } "get_tracks" => Some(Box::new(GetTracksCommand {})), "get_input" => Some(Box::new(GetCurrentInputCommand {})), "get_inputs" => Some(Box::new(GetAllInputsCommand {})), "set_input" => { let name = Some(request.args.get("input_name").unwrap_or(&String::new())).cloned(); Some(Box::new(SetCurrentInputCommand { name })) } "get_output" => Some(Box::new(GetCurrentOutputCommand {})), "get_outputs" => Some(Box::new(GetAllOutputsCommand {})), "set_output" => { let name = Some(request.args.get("output_name").unwrap_or(&String::new())).cloned(); Some(Box::new(SetCurrentOutputCommand { name })) } "set_loop" => { let enabled = request .args .get("enabled") .unwrap_or(&String::new()) .parse::() .ok(); Some(Box::new(SetLoopCommand { enabled, id })) } "toggle_loop" => Some(Box::new(ToggleLoopCommand { id })), "get_daemon_version" => Some(Box::new(GetDaemonVersionCommand {})), "get_full_state" => Some(Box::new(GetFullStateCommand {})), "get_hotkeys" => Some(Box::new(GetHotkeysCommand {})), "set_hotkey" => { let slot = request.args.get("slot").cloned(); let file_path = request .args .get("file_path") .and_then(|s| s.parse::().ok()); Some(Box::new(SetHotkeyCommand { slot, file_path })) } "set_hotkey_key" => { let slot = request.args.get("slot").cloned(); let key_chord = request.args.get("key_chord").cloned(); Some(Box::new(SetHotkeyKeyCommand { slot, key_chord })) } "clear_hotkey" => { let slot = request.args.get("slot").cloned(); Some(Box::new(ClearHotkeyCommand { slot })) } "play_hotkey" => { let slot = request.args.get("slot").cloned(); Some(Box::new(PlayHotkeyCommand { slot })) } "set_hotkey_action" => { let slot = request.args.get("slot").cloned(); let action = request .args .get("action") .and_then(|s| serde_json::from_str::(s).ok()); Some(Box::new(SetHotkeyActionCommand { slot, action })) } "clear_hotkey_key" => { let slot = request.args.get("slot").cloned(); Some(Box::new(ClearHotkeyKeyCommand { slot })) } "set_hotkey_action_and_key" => { let slot = request.args.get("slot").cloned(); let action = request .args .get("action") .and_then(|s| serde_json::from_str::(s).ok()); let key_chord = request.args.get("key_chord").cloned(); Some(Box::new(SetHotkeyActionAndKeyCommand { slot, action, key_chord, })) } "get_daemon_config" => Some(Box::new(GetDaemonConfigCommand {})), "save_daemon_config" => Some(Box::new(SaveDaemonConfigCommand {})), "update_daemon_config" => { let new_config = request .args .get("new_config") .and_then(|nc| serde_json::from_str::(nc).ok()) .unwrap_or_default(); Some(Box::new(UpdateDaemonConfigCommand { new_config })) } _ => None, } } #[cfg(test)] mod tests { use super::*; use crate::types::socket::Request; use std::collections::HashMap; #[test] fn test_parse_set_volume_valid() { let mut args = HashMap::new(); args.insert("volume".to_string(), "0.5".to_string()); args.insert("id".to_string(), "1".to_string()); let request = Request { name: "set_volume".to_string(), args, }; let cmd = parse_command(&request); assert!(cmd.is_some()); } #[test] fn test_parse_set_volume_missing_volume() { let mut args = HashMap::new(); args.insert("id".to_string(), "1".to_string()); let request = Request { name: "set_volume".to_string(), args, }; let cmd = parse_command(&request); assert!(cmd.is_some()); } #[test] fn test_parse_set_volume_invalid_volume() { let mut args = HashMap::new(); args.insert("volume".to_string(), "not-a-float".to_string()); let request = Request { name: "set_volume".to_string(), args, }; let cmd = parse_command(&request); assert!(cmd.is_some()); } #[test] fn test_parse_set_volume_missing_id() { let mut args = HashMap::new(); args.insert("volume".to_string(), "0.5".to_string()); let request = Request { name: "set_volume".to_string(), args, }; let cmd = parse_command(&request); assert!(cmd.is_some()); } #[test] fn test_parse_set_volume_invalid_id() { let mut args = HashMap::new(); args.insert("id".to_string(), "not-an-int".to_string()); args.insert("volume".to_string(), "0.5".to_string()); let request = Request { name: "set_volume".to_string(), args, }; let cmd = parse_command(&request); assert!(cmd.is_some()); } #[test] fn test_parse_set_volume_empty_args() { let request = Request { name: "set_volume".to_string(), args: HashMap::new(), }; let cmd = parse_command(&request); assert!(cmd.is_some()); } #[test] fn test_volume_target_mapping() { assert_eq!( volume_target("set_monitoring_volume"), Some(VolumeTarget::Monitoring) ); assert_eq!( volume_target("get_monitoring_volume"), Some(VolumeTarget::Monitoring) ); assert_eq!(volume_target("set_mic_volume"), Some(VolumeTarget::Mic)); assert_eq!(volume_target("get_mic_volume"), Some(VolumeTarget::Mic)); // The two paths share a dispatch arm, so a name that is neither must not // silently fall through to one of them. assert_eq!(volume_target("set_volume"), None); assert_eq!(volume_target("mic"), None); } #[test] fn test_parse_new_commands() { let with_volume = |name: &str| { let mut args = HashMap::new(); args.insert("volume".to_string(), "2.0".to_string()); Request { name: name.to_string(), args, } }; assert!(parse_command(&with_volume("set_monitoring_volume")).is_some()); assert!(parse_command(&with_volume("set_mic_volume")).is_some()); for name in [ "get_monitoring_volume", "get_mic_volume", "get_output", "get_outputs", ] { let request = Request::new(name, vec![]); assert!( parse_command(&request).is_some(), "{} did not dispatch", name ); } let set_output = Request::set_output("some-sink"); assert!(parse_command(&set_output).is_some()); assert!(parse_command(&Request::new("set_speaker_volume", vec![])).is_none()); } }