Command execution and storage

This commit is contained in:
Aode (lion) 2021-10-06 19:40:22 -05:00
commit 8350dca696
8 changed files with 1523 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
Cargo.lock

20
Cargo.toml Normal file
View file

@ -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"] }

407
src/command.rs Normal file
View file

@ -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<CommandNode>,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize)]
struct CommandNode {
delay: Option<Delay>,
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<C>(command: C) -> Self
where
Command: From<C>,
{
CommandList {
first: command.into(),
rest: Vec::new(),
}
}
pub fn add_command<C>(&mut self, command: C, delay: Option<Duration>) -> &mut Self
where
Command: From<C>,
{
self.rest.push(CommandNode {
delay: delay.map(From::from),
command: command.into(),
});
self
}
pub async fn execute(&self, client: &Client) -> Result<Response, Error> {
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<Duration> for Delay {
fn from(duration: Duration) -> Self {
Delay {
millis: duration.subsec_millis().into(),
seconds: duration.as_secs(),
}
}
}
impl From<GetSceneList> for Command {
fn from(gsl: GetSceneList) -> Self {
gsl.into_command()
}
}
impl From<SwitchScene> for Command {
fn from(ss: SwitchScene) -> Self {
ss.into_command()
}
}
impl From<SetSceneItemVisibility> for Command {
fn from(ssiv: SetSceneItemVisibility) -> Self {
ssiv.into_command()
}
}
impl From<SetRecording> for Command {
fn from(sr: SetRecording) -> Self {
sr.into_command()
}
}
impl From<SetStreaming> for Command {
fn from(ss: SetStreaming) -> Self {
ss.into_command()
}
}

73
src/error.rs Normal file
View file

@ -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<TransactionError<Error>> for Error {
fn from(txerr: TransactionError<Error>) -> Self {
match txerr {
TransactionError::Abort(e) => e,
TransactionError::Storage(e) => e.into(),
}
}
}
impl<T> From<T> for Error
where
ErrorKind: From<T>,
{
fn from(t: T) -> Self {
Error {
kind: ErrorKind::from(t),
context: SpanTrace::capture(),
}
}
}

30
src/lib.rs Normal file
View file

@ -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<Client, Error> {
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)
}

637
src/persist.rs Normal file
View file

@ -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<Self, Error> {
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<Option<CommandList>, 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::<DeckId>()
.push::<Key>()
.field::<CommandField>();
#[cfg(not(release))]
let command_use_parser = PathGen::parser()
.push::<CommandName>()
.push::<UsedBy>()
.push::<DeckId>()
.push::<Key>()
.field::<UsedByField>();
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::<UsedBy>()
.to_bytes();
let key_command_parser = PathGen::parser()
.push::<DeckId>()
.push::<Key>()
.field::<CommandField>();
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::<ParseError>(&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<Option<String>, 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::<CommandName>()
.push::<UsedBy>()
.push::<DeckId>()
.push::<Key>()
.field::<UsedByField>();
if let Some(ivec) = self.commands.get(key_command_path)? {
let command_use_path = command_use_parser.parse::<ParseError>(&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::<Key>()
.to_bytes();
let command_use_parser = PathGen::parser()
.push::<CommandName>()
.push::<UsedBy>()
.push::<DeckId>()
.push::<Key>()
.field::<UsedByField>();
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::<ParseError>(&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<Option<String>, 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<Option<String>, 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::<CommandName>().to_bytes();
let command_parser = PathGen::parser()
.push::<CommandName>()
.field::<CommandField>();
self.commands
.scan_prefix(command_prefix)
.filter_map(|res| res.ok())
.filter_map(|(k, v)| {
let command_name_path = command_parser.parse::<ParseError>(&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<Error> {
trans_err(e.into())
}
fn trans_err(kind: ErrorKind) -> ConflictableTransactionError<Error> {
ConflictableTransactionError::Abort(ErrorKind::from(kind).into())
}
#[cfg(test)]
mod tests {
use crate::{
command::{CommandList, GetSceneList},
persist::Store,
};
use std::convert::TryInto;
fn with_store<O>(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::<Vec<_>>();
let mappings = names
.iter()
.enumerate()
.map(|(index, name)| {
let index: u8 = index.try_into().unwrap();
let key: u8 = index + 1;
(key, *name)
})
.collect::<Vec<_>>();
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::<Vec<_>>();
let mappings = names
.iter()
.enumerate()
.map(|(index, name)| {
let index: u8 = index.try_into().unwrap();
let key: u8 = index + 1;
(key, *name)
})
.collect::<Vec<_>>();
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::<Vec<_>>();
let mappings = names
.iter()
.enumerate()
.map(|(index, name)| {
let index: u8 = index.try_into().unwrap();
let key: u8 = index + 1;
(key, *name)
})
.collect::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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();
}
}
}

304
src/persist/path.rs Normal file
View file

@ -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<Self, Self::Error>
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<Self, Self::Error>
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<Self, Self::Error>
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<Self, Self::Error>
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<Self, Self::Error>
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<Self, Self::Error>
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<Self, Self::Error>
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);
}
}
}

50
src/response.rs Normal file
View file

@ -0,0 +1,50 @@
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
pub struct Response {
items: Vec<ResponseItem>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[serde(tag = "type")]
pub enum ResponseItem {
CurrentScene {
name: String,
},
SceneList {
scenes: Vec<String>,
},
SceneItems {
scene: String,
items: Vec<SceneItem>,
},
}
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<SceneItem>,
}
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(),
}
}
}