use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PluginsConfig {
#[serde(default, alias = "voiceGrants")]
pub voice_grants: HashMap<String, PluginVoiceGrant>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct PluginVoiceGrant {
#[serde(default, alias = "readTranscripts")]
pub read_transcripts: bool,
#[serde(default, alias = "dispatchCommands")]
pub dispatch_commands: bool,
#[serde(default, alias = "synthesizeAudio")]
pub synthesize_audio: bool,
#[serde(default, alias = "transcriptTopics")]
pub transcript_topics: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_config_parses() {
let cfg: PluginsConfig = serde_json::from_str("{}").unwrap();
assert!(cfg.voice_grants.is_empty());
}
#[test]
fn voice_grant_parses_snake_and_camel() {
let json = r#"{
"voice_grants": {
"com.example.snake": {
"read_transcripts": true,
"synthesize_audio": true,
"transcript_topics": ["weftos.voice.transcripts.v1"]
}
}
}"#;
let cfg: PluginsConfig = serde_json::from_str(json).unwrap();
let g = &cfg.voice_grants["com.example.snake"];
assert!(g.read_transcripts);
assert!(g.synthesize_audio);
assert!(!g.dispatch_commands);
assert_eq!(g.transcript_topics, vec!["weftos.voice.transcripts.v1"]);
let json_camel = r#"{
"voiceGrants": {
"com.example.camel": {
"readTranscripts": true,
"dispatchCommands": true,
"transcriptTopics": ["t1", "t2"]
}
}
}"#;
let cfg: PluginsConfig = serde_json::from_str(json_camel).unwrap();
let g = &cfg.voice_grants["com.example.camel"];
assert!(g.read_transcripts);
assert!(g.dispatch_commands);
assert!(!g.synthesize_audio);
assert_eq!(g.transcript_topics, vec!["t1", "t2"]);
}
#[test]
fn missing_fields_default_to_deny() {
let json = r#"{"voice_grants": {"com.example.minimal": {}}}"#;
let cfg: PluginsConfig = serde_json::from_str(json).unwrap();
let g = &cfg.voice_grants["com.example.minimal"];
assert!(!g.read_transcripts);
assert!(!g.dispatch_commands);
assert!(!g.synthesize_audio);
assert!(g.transcript_topics.is_empty());
}
#[test]
fn voice_grant_serde_roundtrip() {
let original = PluginVoiceGrant {
read_transcripts: true,
dispatch_commands: false,
synthesize_audio: true,
transcript_topics: vec!["topic.a".into(), "topic.b".into()],
};
let json = serde_json::to_string(&original).unwrap();
let restored: PluginVoiceGrant = serde_json::from_str(&json).unwrap();
assert_eq!(restored, original);
}
}