From 8350dca69629d19d6451db6ef32939e0a6f15c14 Mon Sep 17 00:00:00 2001 From: "Aode (lion)" Date: Wed, 6 Oct 2021 19:40:22 -0500 Subject: [PATCH] Command execution and storage --- .gitignore | 2 + Cargo.toml | 20 ++ src/command.rs | 407 ++++++++++++++++++++++++++++ src/error.rs | 73 +++++ src/lib.rs | 30 +++ src/persist.rs | 637 ++++++++++++++++++++++++++++++++++++++++++++ src/persist/path.rs | 304 +++++++++++++++++++++ src/response.rs | 50 ++++ 8 files changed, 1523 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 src/command.rs create mode 100644 src/error.rs create mode 100644 src/lib.rs create mode 100644 src/persist.rs create mode 100644 src/persist/path.rs create mode 100644 src/response.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..1f9d9b5 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "obs-commands" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +either = "1.6.1" +obws = "0.8.0" +path-gen = { version = "0.1.0", git = "https://git.asonix.dog/asonix/path-gen", branch = "main" } +serde = { version = "1.0.130", features = ["derive"] } +serde_json = "1.0.68" +sled = "0.34.7" +tokio = { version = "1.12.0", features = ["time"] } +thiserror = "1.0.29" +tracing-error = "0.1.2" + +[dev-dependencies] +path-gen = { version = "0.1.0", git = "https://git.asonix.dog/asonix/path-gen", branch = "main", features = ["test"] } diff --git a/src/command.rs b/src/command.rs new file mode 100644 index 0000000..49b4b78 --- /dev/null +++ b/src/command.rs @@ -0,0 +1,407 @@ +use crate::{ + error::Error, + response::{Response, ResponseItem, SceneItem}, +}; +use obws::Client; +use std::time::Duration; + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize)] +pub struct CommandList { + first: Command, + rest: Vec, +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize)] +struct CommandNode { + delay: Option, + command: Command, +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize)] +struct Delay { + millis: u64, + seconds: u64, +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize)] +#[serde(tag = "type")] +pub enum Command { + GetSceneList(GetSceneList), + SwitchScene(SwitchScene), + SetSceneItemVisibility(SetSceneItemVisibility), + SetStreaming(SetStreaming), + SetRecording(SetRecording), +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize)] +pub struct GetSceneList; + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize)] +pub struct SwitchScene { + to: String, +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize)] +pub struct SetSceneItemVisibility { + scene_name: String, + item_id: i64, + operation: VisibilityOperation, +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize)] +pub struct SetStreaming { + operation: StateOperation, +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize)] +pub struct SetRecording { + operation: StateOperation, +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize)] +pub enum VisibilityOperation { + On, + Off, + Toggle, +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize)] +pub enum StateOperation { + Start, + Stop, + Pause, + Resume, + Toggle, +} + +impl CommandList { + pub fn new(command: C) -> Self + where + Command: From, + { + CommandList { + first: command.into(), + rest: Vec::new(), + } + } + + pub fn add_command(&mut self, command: C, delay: Option) -> &mut Self + where + Command: From, + { + self.rest.push(CommandNode { + delay: delay.map(From::from), + command: command.into(), + }); + self + } + + pub async fn execute(&self, client: &Client) -> Result { + let mut output = Response::default(); + + self.first.execute(&mut output, client).await?; + + for node in &self.rest { + node.execute(&mut output, client).await?; + } + + Ok(output) + } +} + +impl CommandNode { + async fn execute(&self, output: &mut Response, client: &Client) -> Result<(), Error> { + if let Some(delay) = &self.delay { + let duration = Duration::from_secs(delay.seconds) + Duration::from_millis(delay.millis); + tokio::time::sleep(duration).await; + } + + self.command.execute(output, client).await?; + + Ok(()) + } +} + +impl Command { + async fn execute(&self, output: &mut Response, client: &Client) -> Result<(), Error> { + match self { + Command::GetSceneList(get_scene_list) => get_scene_list.execute(output, client).await, + Command::SwitchScene(switch_scene) => switch_scene.execute(output, client).await, + Command::SetSceneItemVisibility(set_scene_item_visibility) => { + set_scene_item_visibility.execute(output, client).await + } + Command::SetRecording(set_recording) => set_recording.execute(output, client).await, + Command::SetStreaming(set_streaming) => set_streaming.execute(output, client).await, + } + } +} + +impl GetSceneList { + pub fn new() -> Self { + GetSceneList + } + + pub fn into_command(self) -> Command { + Command::GetSceneList(self) + } + + async fn execute(&self, output: &mut Response, client: &Client) -> Result<(), Error> { + let scene_list = client.scenes().get_scene_list().await?; + + output.insert(ResponseItem::CurrentScene { + name: scene_list.current_scene, + }); + output.insert(ResponseItem::SceneList { + scenes: scene_list + .scenes + .iter() + .map(|scene| scene.name.clone()) + .collect(), + }); + for scene in scene_list.scenes { + output.insert(ResponseItem::SceneItems { + scene: scene.name, + items: scene + .sources + .into_iter() + .map(SceneItem::translate) + .collect(), + }); + } + + Ok(()) + } +} + +impl SwitchScene { + pub fn to(to: String) -> Self { + SwitchScene { to } + } + + pub fn into_command(self) -> Command { + Command::SwitchScene(self) + } + + async fn execute(&self, output: &mut Response, client: &Client) -> Result<(), Error> { + client.scenes().set_current_scene(&self.to).await?; + + output.insert(ResponseItem::CurrentScene { + name: self.to.clone(), + }); + + Ok(()) + } +} + +impl SetSceneItemVisibility { + pub fn show(scene_name: String, item_id: i64) -> Self { + SetSceneItemVisibility { + scene_name, + item_id, + operation: VisibilityOperation::On, + } + } + + pub fn hide(scene_name: String, item_id: i64) -> Self { + SetSceneItemVisibility { + scene_name, + item_id, + operation: VisibilityOperation::Off, + } + } + + pub fn toggle(scene_name: String, item_id: i64) -> Self { + SetSceneItemVisibility { + scene_name, + item_id, + operation: VisibilityOperation::Toggle, + } + } + + pub fn into_command(self) -> Command { + Command::SetSceneItemVisibility(self) + } + + async fn execute(&self, _output: &mut Response, client: &Client) -> Result<(), Error> { + fn to_specification( + id: i64, + ) -> either::Either<&'static str, obws::requests::SceneItemSpecification<'static>> { + either::Either::Right(obws::requests::SceneItemSpecification { + name: None, + id: Some(id), + }) + } + + let visible = match self.operation { + VisibilityOperation::On => true, + VisibilityOperation::Off => false, + VisibilityOperation::Toggle => { + let properties = client + .scene_items() + .get_scene_item_properties( + Some(&self.scene_name), + to_specification(self.item_id), + ) + .await?; + + !properties.visible + } + }; + + let properties = obws::requests::SceneItemProperties { + scene_name: Some(&self.scene_name), + item: to_specification(self.item_id), + visible: Some(visible), + ..Default::default() + }; + + client + .scene_items() + .set_scene_item_properties(properties) + .await?; + + Ok(()) + } +} + +impl SetRecording { + pub fn start() -> Self { + SetRecording { + operation: StateOperation::Start, + } + } + + pub fn stop() -> Self { + SetRecording { + operation: StateOperation::Stop, + } + } + pub fn pause() -> Self { + SetRecording { + operation: StateOperation::Pause, + } + } + + pub fn resume() -> Self { + SetRecording { + operation: StateOperation::Resume, + } + } + + pub fn toggle() -> Self { + SetRecording { + operation: StateOperation::Toggle, + } + } + + pub fn into_command(self) -> Command { + Command::SetRecording(self) + } + + async fn execute(&self, _output: &mut Response, client: &Client) -> Result<(), Error> { + let status = client.recording().get_recording_status().await?; + + match self.operation { + StateOperation::Start if !status.is_recording => { + client.recording().start_recording().await?; + } + StateOperation::Stop if status.is_recording => { + client.recording().stop_recording().await?; + } + StateOperation::Toggle => { + client.recording().start_stop_recording().await?; + } + StateOperation::Pause if !status.is_recording_paused => { + client.recording().pause_recording().await?; + } + StateOperation::Resume if status.is_recording_paused => { + client.recording().resume_recording().await?; + } + _ => (), + } + + Ok(()) + } +} + +impl SetStreaming { + pub fn start() -> Self { + SetStreaming { + operation: StateOperation::Start, + } + } + + pub fn stop() -> Self { + SetStreaming { + operation: StateOperation::Stop, + } + } + + pub fn toggle() -> Self { + SetStreaming { + operation: StateOperation::Toggle, + } + } + + pub fn into_command(self) -> Command { + Command::SetStreaming(self) + } + + async fn execute(&self, _output: &mut Response, client: &Client) -> Result<(), Error> { + let status = client.streaming().get_streaming_status().await?; + + match self.operation { + StateOperation::Start if !status.streaming => { + client.streaming().start_streaming(None).await?; + } + StateOperation::Stop if status.streaming => { + client.streaming().stop_streaming().await?; + } + StateOperation::Toggle => { + client.streaming().start_stop_streaming().await?; + } + _ => (), + } + + Ok(()) + } +} + +impl From for Delay { + fn from(duration: Duration) -> Self { + Delay { + millis: duration.subsec_millis().into(), + seconds: duration.as_secs(), + } + } +} + +impl From for Command { + fn from(gsl: GetSceneList) -> Self { + gsl.into_command() + } +} + +impl From for Command { + fn from(ss: SwitchScene) -> Self { + ss.into_command() + } +} + +impl From for Command { + fn from(ssiv: SetSceneItemVisibility) -> Self { + ssiv.into_command() + } +} + +impl From for Command { + fn from(sr: SetRecording) -> Self { + sr.into_command() + } +} + +impl From for Command { + fn from(ss: SetStreaming) -> Self { + ss.into_command() + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..7c0a9a5 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,73 @@ +use crate::persist::ParseError; +use sled::transaction::TransactionError; +use tracing_error::SpanTrace; + +#[derive(Debug)] +pub struct Error { + kind: ErrorKind, + context: SpanTrace, +} + +#[derive(Debug, thiserror::Error)] +pub enum ErrorKind { + #[error(transparent)] + Obws(#[from] obws::Error), + + #[error(transparent)] + Sled(#[from] sled::Error), + + #[error(transparent)] + Json(#[from] serde_json::Error), + + #[error(transparent)] + PathBuilding(#[from] ParseError), + + #[error("Authentication required to connect")] + AuthRequired, + + #[error("Command not found")] + Missing, + + #[error("Cannot remove command currently use")] + CommandInUse, +} + +impl Error { + pub fn kind(&self) -> &ErrorKind { + &self.kind + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "{}", self.kind)?; + std::fmt::Display::fmt(&self.context, f) + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + self.kind.source() + } +} + +impl From> for Error { + fn from(txerr: TransactionError) -> Self { + match txerr { + TransactionError::Abort(e) => e, + TransactionError::Storage(e) => e.into(), + } + } +} + +impl From for Error +where + ErrorKind: From, +{ + fn from(t: T) -> Self { + Error { + kind: ErrorKind::from(t), + context: SpanTrace::capture(), + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..2a221a6 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,30 @@ +mod command; +mod error; +mod persist; +mod response; + +pub use command::{ + Command, CommandList, GetSceneList, SetRecording, SetSceneItemVisibility, SetStreaming, + SwitchScene, +}; +pub use error::{Error, ErrorKind}; +pub use persist::Store; +pub use response::{Response, ResponseItem, SceneItem}; + +use obws::Client; + +pub async fn connect(host: &str, port: u16, password: Option<&str>) -> Result { + let mut client = Client::connect(host, port).await?; + + if let Some(password) = password { + client.login(Some(password)).await?; + } else { + let auth_required = client.general().get_auth_required().await?; + if auth_required.auth_required { + client.disconnect().await; + return Err(ErrorKind::AuthRequired.into()); + } + } + + Ok(client) +} diff --git a/src/persist.rs b/src/persist.rs new file mode 100644 index 0000000..6baf3aa --- /dev/null +++ b/src/persist.rs @@ -0,0 +1,637 @@ +use crate::{ + command::CommandList, + error::{Error, ErrorKind}, +}; +use path_gen::PathGen; +use sled::{transaction::ConflictableTransactionError, Db, Tree}; + +mod path; + +use path::{CommandField, CommandName, DeckId, Key, NameField, UsedBy, UsedByField, PATH_ROOT}; + +pub use path::{ + CommandFieldParseError, NameFieldParseError, ParseError, UsedByFieldParseError, + UsedByParseError, +}; + +#[derive(Clone)] +pub struct Store { + commands: Tree, + _db: Db, +} + +impl Store { + pub fn build(db: Db) -> Result { + let commands = db.open_tree("obs-commands.commands-store")?; + + // read everything to memory + for res in commands.iter() { + let _ = res?; + } + + Ok(Store { commands, _db: db }) + } + + pub fn save_command( + &self, + command_name: &str, + command_list: &CommandList, + ) -> Result<(), Error> { + let v = serde_json::to_vec(command_list)?; + + let command_name_path = PATH_ROOT + .push(CommandName::new(command_name)) + .field(CommandField) + .to_bytes(); + + self.commands.insert(command_name_path, v)?; + + Ok(()) + } + + pub fn get_command(&self, command_name: &str) -> Result, Error> { + let command_name_path = PATH_ROOT + .push(CommandName::new(command_name)) + .field(CommandField) + .to_bytes(); + + if let Some(ivec) = self.commands.get(command_name_path)? { + return Ok(Some(serde_json::from_slice(&ivec)?)); + } + + Ok(None) + } + + pub fn map_command(&self, deck_id: &str, key: u8, command_name: &str) -> Result<(), Error> { + let command_key = PATH_ROOT + .push(CommandName::new(command_name)) + .field(CommandField) + .to_bytes(); + + let key_command_path = PATH_ROOT + .push(DeckId::new(deck_id)) + .push(Key::new(key)) + .field(CommandField) + .to_bytes(); + + let command_use_path = PATH_ROOT + .push(CommandName::new(command_name)) + .push(UsedBy) + .push(DeckId::new(deck_id)) + .push(Key::new(key)) + .field(UsedByField) + .to_bytes(); + + #[cfg(not(release))] + let key_command_parser = PathGen::parser() + .push::() + .push::() + .field::(); + + #[cfg(not(release))] + let command_use_parser = PathGen::parser() + .push::() + .push::() + .push::() + .push::() + .field::(); + + self.commands.transaction(|commands| { + if commands.get(&command_key)?.is_none() { + return Err(trans_err(ErrorKind::Missing)); + } + + if let Some(previous_command_use_path) = commands.get(&key_command_path)? { + #[cfg(not(release))] + let previous_command_use_path = command_use_parser + .parse(&previous_command_use_path) + .map_err(trans_parse_err)? + .to_bytes(); + + if let Some(previous_key_command_path) = + commands.remove(previous_command_use_path)? + { + #[cfg(not(release))] + let previous_key_command_path = key_command_parser + .parse(&previous_key_command_path) + .map_err(trans_parse_err)? + .to_bytes(); + + commands.remove(previous_key_command_path)?; + } + } + + commands.insert(key_command_path.as_slice(), command_use_path.as_slice())?; + commands.insert(command_use_path.as_slice(), key_command_path.as_slice())?; + Ok(()) + })?; + + Ok(()) + } + + // TODO: Figure out transactions for this + pub fn remove_command(&self, command_name: &str) -> Result<(), Error> { + if !self.get_command_uses(command_name).is_empty() { + return Err(ErrorKind::CommandInUse.into()); + } + + let command_key = PATH_ROOT + .push(CommandName::new(command_name)) + .field(CommandField) + .to_bytes(); + + self.commands.remove(command_key)?; + + Ok(()) + } + + pub fn get_command_uses(&self, command_name: &str) -> Vec<(String, u8)> { + let command_use_prefix = PATH_ROOT + .push(CommandName::new(command_name)) + .prefix::() + .to_bytes(); + + let key_command_parser = PathGen::parser() + .push::() + .push::() + .field::(); + + self.commands + .scan_prefix(&command_use_prefix) + .values() + .filter_map(|res| res.ok()) + .filter_map(|key_command_map| { + let key_command_path = key_command_parser + .parse::(&key_command_map) + .ok()?; + + let key = key_command_path.parent().key(); + let deck_id = key_command_path.parent().parent().deck_id().to_string(); + + Some((deck_id, key)) + }) + .collect() + } + + pub fn get_mapped_command(&self, deck_id: &str, key: u8) -> Result, Error> { + let key_command_path = PATH_ROOT + .push(DeckId::new(deck_id)) + .push(Key::new(key)) + .field(CommandField) + .to_bytes(); + + let command_use_parser = PathGen::parser() + .push::() + .push::() + .push::() + .push::() + .field::(); + + if let Some(ivec) = self.commands.get(key_command_path)? { + let command_use_path = command_use_parser.parse::(&ivec)?; + + let command_name = command_use_path + .parent() + .parent() + .parent() + .parent() + .command_name() + .to_string(); + + return Ok(Some(command_name)); + } + + Ok(None) + } + + pub fn mappings_for_deck(&self, deck_id: &str) -> Vec<(u8, String)> { + let key_prefix_for_deck = PATH_ROOT + .push(DeckId::new(deck_id)) + .prefix::() + .to_bytes(); + + let command_use_parser = PathGen::parser() + .push::() + .push::() + .push::() + .push::() + .field::(); + + self.commands + .scan_prefix(key_prefix_for_deck) + .values() + .filter_map(|res| res.ok()) + .filter_map(|v| { + let command_use_path = command_use_parser.parse::(&v).ok()?; + + let key = command_use_path.parent().key(); + let command_name = command_use_path + .parent() + .parent() + .parent() + .parent() + .command_name() + .to_string(); + + Some((key, command_name)) + }) + .collect() + } + + pub fn save_deck_name(&self, deck_id: &str, name: &str) -> Result<(), Error> { + let deck_name_path = PATH_ROOT + .push(DeckId::new(deck_id)) + .field(NameField) + .to_bytes(); + + self.commands.insert(deck_name_path, name.as_bytes())?; + Ok(()) + } + + pub fn get_deck_name(&self, deck_id: &str) -> Result, Error> { + let deck_name_path = PATH_ROOT + .push(DeckId::new(deck_id)) + .field(NameField) + .to_bytes(); + + if let Some(ivec) = self.commands.get(deck_name_path)? { + let string = String::from_utf8(ivec.to_vec()).map_err(ParseError::from)?; + + return Ok(Some(string)); + } + + Ok(None) + } + + pub fn save_key_name(&self, deck_id: &str, key: u8, name: &str) -> Result<(), Error> { + let key_name_path = PATH_ROOT + .push(DeckId::new(deck_id)) + .push(Key::new(key)) + .field(NameField) + .to_bytes(); + + self.commands.insert(key_name_path, name.as_bytes())?; + Ok(()) + } + + pub fn get_key_name(&self, deck_id: &str, key: u8) -> Result, Error> { + let key_name_path = PATH_ROOT + .push(DeckId::new(deck_id)) + .push(Key::new(key)) + .field(NameField) + .to_bytes(); + + if let Some(ivec) = self.commands.get(key_name_path)? { + let string = String::from_utf8(ivec.to_vec()).map_err(ParseError::from)?; + + return Ok(Some(string)); + } + + Ok(None) + } + + pub fn all_commands(&self) -> Vec<(String, CommandList)> { + let command_prefix = PATH_ROOT.prefix::().to_bytes(); + let command_parser = PathGen::parser() + .push::() + .field::(); + + self.commands + .scan_prefix(command_prefix) + .filter_map(|res| res.ok()) + .filter_map(|(k, v)| { + let command_name_path = command_parser.parse::(&k).ok()?; + let command_list = serde_json::from_slice(&v).ok()?; + + let command_name = command_name_path.parent().command_name().to_string(); + + Some((command_name, command_list)) + }) + .collect() + } +} + +impl std::fmt::Debug for Store { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Store").finish() + } +} + +#[cfg(not(release))] +fn trans_parse_err(e: ParseError) -> ConflictableTransactionError { + trans_err(e.into()) +} + +fn trans_err(kind: ErrorKind) -> ConflictableTransactionError { + ConflictableTransactionError::Abort(ErrorKind::from(kind).into()) +} + +#[cfg(test)] +mod tests { + use crate::{ + command::{CommandList, GetSceneList}, + persist::Store, + }; + use std::convert::TryInto; + + fn with_store(f: impl Fn(Store) -> O) -> O { + let db = sled::Config::new().temporary(true).open().unwrap(); + let store = Store::build(db).unwrap(); + + (f)(store) + } + + #[test] + fn store_and_retrieve_command() { + let cmd = CommandList::new(GetSceneList::new()); + let name = "store_and_retrieve_command"; + + let new_cmd = with_store(|store| { + store.save_command(name, &cmd).unwrap(); + store.get_command(name).unwrap().unwrap() + }); + + assert_eq!(cmd, new_cmd); + } + + #[test] + fn map_key_to_command() { + let cmd = CommandList::new(GetSceneList::new()); + let name = "map_key_to_command"; + let key = 1; + let deck_id = "012345"; + + let new_name = with_store(|store| { + store.save_command(name, &cmd).unwrap(); + store.map_command(deck_id, key, name).unwrap(); + store.get_mapped_command(deck_id, key).unwrap().unwrap() + }); + + assert_eq!(new_name, name) + } + + #[test] + fn doesnt_map_missing_command() { + let name = "doesnt_map_missing_command"; + let key = 1; + let deck_id = "012345"; + + let result = with_store(|store| store.map_command(deck_id, key, name)); + + assert!(result.is_err()); + } + + #[test] + fn adds_use_for_mapped_command() { + let cmd = CommandList::new(GetSceneList::new()); + let name = "adds_use_for_mapped_command"; + let key = 1; + let deck_id = "012345"; + + let uses = with_store(|store| { + store.save_command(name, &cmd).unwrap(); + store.map_command(deck_id, key, name).unwrap(); + store.get_command_uses(name) + }); + + assert_eq!(uses.len(), 1); + assert_eq!(uses[0].0, deck_id); + assert_eq!(uses[0].1, key); + } + + #[test] + fn remove_unused_command() { + let cmd = CommandList::new(GetSceneList::new()); + let name = "remove_unused_command"; + + let opt = with_store(|store| { + store.save_command(name, &cmd).unwrap(); + store.remove_command(name).unwrap(); + store.get_command(name).unwrap() + }); + + assert!(opt.is_none()); + } + + #[test] + fn dont_remove_used_command() { + let cmd = CommandList::new(GetSceneList::new()); + let name = "dont_remove_used_command"; + let deck_id = "012345"; + let key = 1; + + let res = with_store(|store| { + store.save_command(name, &cmd).unwrap(); + store.map_command(deck_id, key, name).unwrap(); + store.remove_command(name) + }); + + assert!(res.is_err()); + } + + #[test] + fn get_mappings_for_deck() { + let names = [ + "get_mappings_for_deck_1", + "get_mappings_for_deck_2", + "get_mappings_for_deck_3", + ]; + let cmds = names + .iter() + .map(|name| (*name, CommandList::new(GetSceneList::new()))) + .collect::>(); + + let mappings = names + .iter() + .enumerate() + .map(|(index, name)| { + let index: u8 = index.try_into().unwrap(); + let key: u8 = index + 1; + + (key, *name) + }) + .collect::>(); + + let deck_id = "012345"; + + let new_mappings = with_store(|store| { + for (name, cmd) in &cmds { + store.save_command(name, cmd).unwrap(); + } + + for (key, name) in &mappings { + store.map_command(deck_id, *key, name).unwrap(); + } + + store.mappings_for_deck(deck_id) + }); + + assert_eq!(new_mappings.len(), mappings.len()); + + for (key, name) in new_mappings { + mappings + .iter() + .find(|(left_key, left_name)| *left_key == key && *left_name == name) + .unwrap(); + } + } + + #[test] + fn doesnt_get_mappings_for_wrong_deck() { + let names = [ + "get_mappings_for_deck_1", + "get_mappings_for_deck_2", + "get_mappings_for_deck_3", + ]; + let cmds = names + .iter() + .map(|name| (*name, CommandList::new(GetSceneList::new()))) + .collect::>(); + + let mappings = names + .iter() + .enumerate() + .map(|(index, name)| { + let index: u8 = index.try_into().unwrap(); + let key: u8 = index + 1; + + (key, *name) + }) + .collect::>(); + + let deck_id = "012345"; + let wrong_deck_id = "543210"; + + let new_mappings = with_store(|store| { + for (name, cmd) in &cmds { + store.save_command(name, cmd).unwrap(); + } + + for (key, name) in &mappings { + store.map_command(deck_id, *key, name).unwrap(); + } + + store.mappings_for_deck(wrong_deck_id) + }); + + assert_eq!(new_mappings.len(), 0); + } + + #[test] + fn save_and_retrieve_deck_name() { + let deck_name = "Some Name"; + let deck_id = "012345"; + + let new_deck_name = with_store(|store| { + store.save_deck_name(deck_id, deck_name).unwrap(); + store.get_deck_name(deck_id).unwrap().unwrap() + }); + + assert_eq!(new_deck_name, deck_name); + } + + #[test] + fn save_and_retrieve_key_name() { + let key_name = "Top Left"; + let deck_id = "012345"; + let key = 1; + + let new_key_name = with_store(|store| { + store.save_key_name(deck_id, key, key_name).unwrap(); + store.get_key_name(deck_id, key).unwrap().unwrap() + }); + + assert_eq!(new_key_name, key_name); + } + + #[test] + fn key_name_doesnt_interfere_with_mappings() { + let names = [ + "get_mappings_for_deck_1", + "get_mappings_for_deck_2", + "get_mappings_for_deck_3", + ]; + let cmds = names + .iter() + .map(|name| (*name, CommandList::new(GetSceneList::new()))) + .collect::>(); + + let mappings = names + .iter() + .enumerate() + .map(|(index, name)| { + let index: u8 = index.try_into().unwrap(); + let key: u8 = index + 1; + + (key, *name) + }) + .collect::>(); + + let key_names = names + .iter() + .enumerate() + .map(|(index, _)| { + let index: u8 = index.try_into().unwrap(); + let key: u8 = index + 1; + + (key, format!("Key {}", key)) + }) + .collect::>(); + + let deck_id = "012345"; + + let new_mappings = with_store(|store| { + for (name, cmd) in &cmds { + store.save_command(name, cmd).unwrap(); + } + + for (key, name) in &mappings { + store.map_command(deck_id, *key, name).unwrap(); + } + + for (key, name) in &key_names { + store.save_key_name(deck_id, *key, name).unwrap(); + } + + store.mappings_for_deck(deck_id) + }); + + assert_eq!(new_mappings.len(), mappings.len()); + + for (key, name) in new_mappings { + mappings + .iter() + .find(|(left_key, left_name)| *left_key == key && *left_name == name) + .unwrap(); + } + } + + #[test] + fn store_and_retrieve_many_commands() { + let names = [ + "get_mappings_for_deck_1", + "get_mappings_for_deck_2", + "get_mappings_for_deck_3", + ]; + let cmds = names + .iter() + .map(|name| (*name, CommandList::new(GetSceneList::new()))) + .collect::>(); + + let new_commands = with_store(|store| { + for (name, cmd) in &cmds { + store.save_command(name, cmd).unwrap(); + } + + store.all_commands() + }); + + assert_eq!(new_commands.len(), 3); + + for (name, command) in new_commands { + cmds.iter() + .find(|(left_name, left_cmd)| *left_name == name && *left_cmd == command) + .unwrap(); + } + } +} diff --git a/src/persist/path.rs b/src/persist/path.rs new file mode 100644 index 0000000..d07ca9f --- /dev/null +++ b/src/persist/path.rs @@ -0,0 +1,304 @@ +use path_gen::{DeconstructError, PathGen}; +use std::{borrow::Cow, str::Utf8Error, string::FromUtf8Error}; + +pub(super) const PATH_ROOT: PathGen<'static> = PathGen::new("obs-commands"); + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(super) struct CommandField; + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, thiserror::Error)] +#[error("Invalid command field")] +pub struct CommandFieldParseError; + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(super) struct CommandName<'a> { + inner: Cow<'a, str>, +} + +impl<'a> CommandName<'a> { + pub(super) const fn new(s: &'a str) -> Self { + CommandName { + inner: Cow::Borrowed(s), + } + } + + fn new_owned(s: String) -> Self { + CommandName { + inner: Cow::Owned(s), + } + } + + pub(super) fn command_name(&self) -> &str { + &self.inner + } +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(super) struct DeckId<'a> { + inner: Cow<'a, str>, +} + +impl<'a> DeckId<'a> { + pub(super) const fn new(s: &'a str) -> Self { + DeckId { + inner: Cow::Borrowed(s), + } + } + + fn new_owned(s: String) -> Self { + DeckId { + inner: Cow::Owned(s), + } + } + + pub(super) fn deck_id(&self) -> &str { + &self.inner + } +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(super) struct Key { + inner: u8, +} + +impl Key { + pub(super) fn new(key: u8) -> Self { + Key { inner: key } + } + + pub(super) fn key(&self) -> u8 { + self.inner + } +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, thiserror::Error)] +#[error("Invalid key")] +pub struct ParseKeyError; + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(super) struct NameField; + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, thiserror::Error)] +#[error("Invalid name field")] +pub struct NameFieldParseError; + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(super) struct UsedBy; + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, thiserror::Error)] +#[error("Invalid UseBy node")] +pub struct UsedByParseError; + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(super) struct UsedByField; + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, thiserror::Error)] +#[error("Invalid use-by field")] +pub struct UsedByFieldParseError; + +#[derive(Debug, thiserror::Error)] +pub enum ParseError { + #[error(transparent)] + Command(#[from] CommandFieldParseError), + + #[error(transparent)] + Name(#[from] NameFieldParseError), + + #[error(transparent)] + UseBy(#[from] UsedByParseError), + + #[error(transparent)] + UseByField(#[from] UsedByFieldParseError), + + #[error(transparent)] + Key(#[from] ParseKeyError), + + #[error(transparent)] + Utf8(#[from] Utf8Error), + + #[error(transparent)] + FromUtf8(#[from] FromUtf8Error), + + #[error(transparent)] + Path(#[from] DeconstructError), +} + +mod implementation { + use super::{ + CommandField, CommandFieldParseError, CommandName, DeckId, Key, NameField, + NameFieldParseError, ParseError, ParseKeyError, UsedBy, UsedByField, UsedByFieldParseError, + UsedByParseError, + }; + use path_gen::{PathField, PathItem, PathNode}; + use std::{borrow::Cow, str::from_utf8}; + + impl<'a> PathItem<'a> for CommandField { + type Error = CommandFieldParseError; + + fn parse(bytes: Cow<'a, [u8]>) -> Result + where + Self: Sized + 'a, + { + if bytes.as_ref() == b"command" { + return Ok(CommandField); + } + + Err(CommandFieldParseError) + } + + fn to_bytes<'b>(&'b self) -> Cow<'b, [u8]> { + Cow::Borrowed(b"command") + } + } + + impl<'a> PathItem<'a> for CommandName<'a> { + type Error = ParseError; + + fn parse(bytes: Cow<'a, [u8]>) -> Result + where + Self: Sized + 'a, + { + match bytes { + Cow::Borrowed(bytes) => Ok(CommandName::new(from_utf8(bytes)?)), + Cow::Owned(bytes) => Ok(CommandName::new_owned(String::from_utf8(bytes)?)), + } + } + + fn to_bytes<'b>(&'b self) -> Cow<'b, [u8]> { + self.inner.as_bytes().into() + } + } + + impl<'a> PathItem<'a> for DeckId<'a> { + type Error = ParseError; + + fn parse(bytes: Cow<'a, [u8]>) -> Result + where + Self: Sized + 'a, + { + match bytes { + Cow::Borrowed(bytes) => Ok(DeckId::new(from_utf8(bytes)?)), + Cow::Owned(bytes) => Ok(DeckId::new_owned(String::from_utf8(bytes)?)), + } + } + + fn to_bytes<'b>(&'b self) -> Cow<'b, [u8]> { + self.inner.as_bytes().into() + } + } + + impl<'a> PathItem<'a> for Key { + type Error = ParseKeyError; + + fn parse(bytes: Cow<'a, [u8]>) -> Result + where + Self: Sized + 'a, + { + if bytes.len() != 1 { + return Err(ParseKeyError); + } + + Ok(Key::new(bytes[0])) + } + + fn to_bytes<'b>(&'b self) -> Cow<'b, [u8]> { + Cow::Owned(vec![self.key()]) + } + } + + impl<'a> PathItem<'a> for NameField { + type Error = NameFieldParseError; + + fn parse(bytes: Cow<'a, [u8]>) -> Result + where + Self: Sized + 'a, + { + if bytes.as_ref() == b"name" { + return Ok(NameField); + } + + Err(NameFieldParseError) + } + + fn to_bytes<'b>(&'b self) -> Cow<'b, [u8]> { + Cow::Borrowed(b"name") + } + } + + impl<'a> PathItem<'a> for UsedBy { + type Error = UsedByParseError; + + fn parse(bytes: Cow<'a, [u8]>) -> Result + where + Self: Sized + 'a, + { + if bytes.as_ref() == b"by" { + return Ok(UsedBy); + } + + Err(UsedByParseError) + } + + fn to_bytes<'b>(&'b self) -> Cow<'b, [u8]> { + Cow::Borrowed(b"by") + } + } + + impl<'a> PathItem<'a> for UsedByField { + type Error = UsedByFieldParseError; + + fn parse(bytes: Cow<'a, [u8]>) -> Result + where + Self: Sized + 'a, + { + if bytes.as_ref() == b"used-by" { + return Ok(UsedByField); + } + + Err(UsedByFieldParseError) + } + + fn to_bytes<'b>(&'b self) -> Cow<'b, [u8]> { + Cow::Borrowed(b"used-by") + } + } + + impl<'a> PathNode<'a> for CommandName<'a> { + const NAME: &'static [u8] = b"command"; + } + + impl<'a> PathNode<'a> for DeckId<'a> { + const NAME: &'static [u8] = b"deck-id"; + } + + impl<'a> PathNode<'a> for Key { + const NAME: &'static [u8] = b"key"; + } + + impl<'a> PathNode<'a> for UsedBy { + const NAME: &'static [u8] = b"used"; + } + + impl<'a> PathField<'a> for CommandField {} + impl<'a> PathField<'a> for NameField {} + impl<'a> PathField<'a> for UsedByField {} + + #[cfg(test)] + mod tests { + use path_gen::test::{test_field, test_pathnode}; + + use super::{CommandField, CommandName, DeckId, Key, NameField, UsedBy, UsedByField}; + + #[test] + fn test_path_components() { + test_pathnode::<_, DeckId, _>(DeckId::new("test")); + test_pathnode::<_, CommandName, _>(CommandName::new("test")); + test_pathnode::<_, Key, _>(Key::new(5)); + test_pathnode::<_, UsedBy, _>(UsedBy); + + test_field::<_, CommandField, _>(CommandField); + test_field::<_, NameField, _>(NameField); + test_field::<_, UsedByField, _>(UsedByField); + } + } +} diff --git a/src/response.rs b/src/response.rs new file mode 100644 index 0000000..fd39c53 --- /dev/null +++ b/src/response.rs @@ -0,0 +1,50 @@ +#[derive(Debug, Default, serde::Deserialize, serde::Serialize)] +pub struct Response { + items: Vec, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +#[serde(tag = "type")] +pub enum ResponseItem { + CurrentScene { + name: String, + }, + SceneList { + scenes: Vec, + }, + SceneItems { + scene: String, + items: Vec, + }, +} + +impl Response { + pub(crate) fn insert(&mut self, item: ResponseItem) { + self.items.push(item); + } +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct SceneItem { + name: String, + id: i64, + ty: String, + visible: bool, + children: Vec, +} + +impl SceneItem { + pub(crate) fn translate(scene_item: obws::common::SceneItem) -> Self { + SceneItem { + name: scene_item.name, + id: scene_item.id, + ty: scene_item.ty, + visible: scene_item.render, + children: scene_item + .group_children + .into_iter() + .map(Self::translate) + .collect(), + } + } +}