Skip to main content

sonos_cli/cli/
resolve.rs

1use sonos_sdk::{Group, SonosSystem, Speaker};
2
3use crate::cli::GlobalFlags;
4use crate::config::Config;
5use crate::errors::CliError;
6
7/// Resolve --speaker / --group flags to a Speaker handle.
8///
9/// Priority: --group wins over --speaker. If neither is given, uses config default
10/// or falls back to the first available speaker.
11pub fn resolve_speaker(
12    system: &SonosSystem,
13    config: &Config,
14    global: &GlobalFlags,
15) -> Result<Speaker, CliError> {
16    // --group wins over --speaker
17    if let Some(group_name) = &global.group {
18        let g = system
19            .group(group_name)
20            .ok_or_else(|| CliError::GroupNotFound(group_name.to_string()))?;
21        return g
22            .coordinator()
23            .ok_or_else(|| CliError::GroupNotFound(group_name.to_string()));
24    }
25
26    if let Some(speaker_name) = &global.speaker {
27        return system
28            .speaker(speaker_name)
29            .ok_or_else(|| CliError::SpeakerNotFound(speaker_name.to_string()));
30    }
31
32    // Default: config group → first speaker
33    if let Some(default_group) = &config.default_group {
34        if let Some(g) = system.group(default_group) {
35            if let Some(coordinator) = g.coordinator() {
36                return Ok(coordinator);
37            }
38        }
39    }
40
41    // Last resort: first speaker
42    system
43        .speakers()
44        .into_iter()
45        .next()
46        .ok_or_else(|| CliError::SpeakerNotFound("no speakers available".to_string()))
47}
48
49/// Resolve --group / --speaker flags to a Group handle.
50///
51/// Priority: --group wins. If neither flag is given, uses config default
52/// or falls back to the first available group.
53pub fn resolve_group(
54    system: &SonosSystem,
55    config: &Config,
56    global: &GlobalFlags,
57) -> Result<Group, CliError> {
58    if let Some(group_name) = &global.group {
59        return system
60            .group(group_name)
61            .ok_or_else(|| CliError::GroupNotFound(group_name.to_string()));
62    }
63
64    // Default: config group → first group
65    if let Some(default_group) = &config.default_group {
66        if let Some(g) = system.group(default_group) {
67            return Ok(g);
68        }
69    }
70
71    system
72        .groups()
73        .into_iter()
74        .next()
75        .ok_or_else(|| CliError::GroupNotFound("no groups available".to_string()))
76}
77
78/// Resolve --speaker flag for speaker-only commands (bass, treble, loudness).
79/// Rejects --group with a validation error.
80pub fn require_speaker_only(
81    system: &SonosSystem,
82    global: &GlobalFlags,
83    command_name: &str,
84) -> Result<Speaker, CliError> {
85    if global.group.is_some() {
86        return Err(CliError::Validation(format!(
87            "--speaker is required for {command_name}"
88        )));
89    }
90    let name = global
91        .speaker
92        .as_deref()
93        .ok_or_else(|| CliError::Validation(format!("--speaker is required for {command_name}")))?;
94    system
95        .speaker(name)
96        .ok_or_else(|| CliError::SpeakerNotFound(name.to_string()))
97}
98
99#[cfg(all(test, feature = "test-helpers"))]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn resolve_speaker_by_name() {
105        let system = SonosSystem::with_speakers(&["Kitchen"]);
106        let config = Config::default();
107        let global = GlobalFlags {
108            speaker: Some("Kitchen".into()),
109            group: None,
110            quiet: false,
111            verbose: false,
112            no_input: false,
113        };
114        let spk = resolve_speaker(&system, &config, &global).unwrap();
115        assert_eq!(spk.name, "Kitchen");
116    }
117
118    #[test]
119    fn resolve_speaker_not_found() {
120        let system = SonosSystem::with_speakers(&["Kitchen"]);
121        let config = Config::default();
122        let global = GlobalFlags {
123            speaker: Some("Nonexistent".into()),
124            group: None,
125            quiet: false,
126            verbose: false,
127            no_input: false,
128        };
129        let result = resolve_speaker(&system, &config, &global);
130        assert!(matches!(result, Err(CliError::SpeakerNotFound(_))));
131    }
132
133    #[test]
134    fn resolve_speaker_falls_back_to_first() {
135        let system = SonosSystem::with_speakers(&["Kitchen"]);
136        let config = Config::default();
137        let global = GlobalFlags {
138            speaker: None,
139            group: None,
140            quiet: false,
141            verbose: false,
142            no_input: false,
143        };
144        let spk = resolve_speaker(&system, &config, &global).unwrap();
145        assert_eq!(spk.name, "Kitchen");
146    }
147
148    #[test]
149    fn resolve_speaker_empty_system_fails() {
150        let system = SonosSystem::with_speakers(&[]);
151        let config = Config::default();
152        let global = GlobalFlags {
153            speaker: None,
154            group: None,
155            quiet: false,
156            verbose: false,
157            no_input: false,
158        };
159        let result = resolve_speaker(&system, &config, &global);
160        assert!(result.is_err());
161    }
162
163    #[test]
164    fn require_speaker_only_rejects_group() {
165        let system = SonosSystem::with_speakers(&["Kitchen"]);
166        let global = GlobalFlags {
167            speaker: None,
168            group: Some("Living Room".into()),
169            quiet: false,
170            verbose: false,
171            no_input: false,
172        };
173        let result = require_speaker_only(&system, &global, "bass");
174        assert!(matches!(result, Err(CliError::Validation(_))));
175    }
176
177    #[test]
178    fn require_speaker_only_requires_speaker_flag() {
179        let system = SonosSystem::with_speakers(&["Kitchen"]);
180        let global = GlobalFlags {
181            speaker: None,
182            group: None,
183            quiet: false,
184            verbose: false,
185            no_input: false,
186        };
187        let result = require_speaker_only(&system, &global, "bass");
188        assert!(matches!(result, Err(CliError::Validation(_))));
189    }
190
191    #[test]
192    fn require_speaker_only_finds_speaker() {
193        let system = SonosSystem::with_speakers(&["Kitchen"]);
194        let global = GlobalFlags {
195            speaker: Some("Kitchen".into()),
196            group: None,
197            quiet: false,
198            verbose: false,
199            no_input: false,
200        };
201        let spk = require_speaker_only(&system, &global, "bass").unwrap();
202        assert_eq!(spk.name, "Kitchen");
203    }
204
205    #[test]
206    fn resolve_group_by_name() {
207        let system = SonosSystem::with_groups(&["Kitchen", "Bedroom"]);
208        let config = Config::default();
209        let global = GlobalFlags {
210            speaker: None,
211            group: Some("Kitchen".into()),
212            quiet: false,
213            verbose: false,
214            no_input: false,
215        };
216        let grp = resolve_group(&system, &config, &global).unwrap();
217        let coord = grp.coordinator().unwrap();
218        assert_eq!(coord.name, "Kitchen");
219    }
220
221    #[test]
222    fn resolve_group_not_found() {
223        let system = SonosSystem::with_groups(&["Kitchen"]);
224        let config = Config::default();
225        let global = GlobalFlags {
226            speaker: None,
227            group: Some("Nonexistent".into()),
228            quiet: false,
229            verbose: false,
230            no_input: false,
231        };
232        let result = resolve_group(&system, &config, &global);
233        assert!(matches!(result, Err(CliError::GroupNotFound(_))));
234    }
235
236    #[test]
237    fn resolve_group_falls_back_to_first() {
238        let system = SonosSystem::with_groups(&["Kitchen"]);
239        let config = Config::default();
240        let global = GlobalFlags {
241            speaker: None,
242            group: None,
243            quiet: false,
244            verbose: false,
245            no_input: false,
246        };
247        let grp = resolve_group(&system, &config, &global).unwrap();
248        let coord = grp.coordinator().unwrap();
249        assert_eq!(coord.name, "Kitchen");
250    }
251
252    #[test]
253    fn resolve_group_uses_config_default() {
254        let system = SonosSystem::with_groups(&["Kitchen", "Bedroom"]);
255        let config = Config {
256            default_group: Some("Bedroom".into()),
257            ..Config::default()
258        };
259        let global = GlobalFlags {
260            speaker: None,
261            group: None,
262            quiet: false,
263            verbose: false,
264            no_input: false,
265        };
266        let grp = resolve_group(&system, &config, &global).unwrap();
267        let coord = grp.coordinator().unwrap();
268        assert_eq!(coord.name, "Bedroom");
269    }
270
271    #[test]
272    fn resolve_group_empty_system_fails() {
273        let system = SonosSystem::with_groups(&[]);
274        let config = Config::default();
275        let global = GlobalFlags {
276            speaker: None,
277            group: None,
278            quiet: false,
279            verbose: false,
280            no_input: false,
281        };
282        let result = resolve_group(&system, &config, &global);
283        assert!(matches!(result, Err(CliError::GroupNotFound(_))));
284    }
285
286    #[test]
287    fn resolve_group_flag_wins_over_speaker() {
288        let system = SonosSystem::with_groups(&["Kitchen", "Bedroom"]);
289        let config = Config::default();
290        let global = GlobalFlags {
291            speaker: Some("Bedroom".into()),
292            group: Some("Kitchen".into()),
293            quiet: false,
294            verbose: false,
295            no_input: false,
296        };
297        let grp = resolve_group(&system, &config, &global).unwrap();
298        let coord = grp.coordinator().unwrap();
299        assert_eq!(coord.name, "Kitchen");
300    }
301}