Skip to main content

sim_lib_stream_host/
config_probe.rs

1//! Configuration probes for stream-host defaults.
2
3use sim_config::{
4    ConfigDir, ConfigLayer, ConfigProbe, ConfigProbeReport, ConfigProbeRequest, ConfigProbeStatus,
5    ConfigSource, ProbeMode,
6};
7use sim_kernel::{Expr, NumberLiteral, Symbol};
8
9use crate::DeviceCatalog;
10
11const DEFAULT_SAMPLE_RATE_HZ: u32 = 48_000;
12const DEFAULT_MAX_BLOCK_FRAMES: u32 = 512;
13
14const EMITTED_KEYS: [&str; 6] = [
15    "audio_backend_candidates",
16    "midi_backend_candidates",
17    "audio_backend_regex",
18    "midi_backend_regex",
19    "sample_rate_hz",
20    "max_block_frames",
21];
22
23/// Returns the stable config library id for stream-host defaults.
24pub fn stream_host_config_lib_symbol() -> Symbol {
25    Symbol::qualified("stream", "host")
26}
27
28/// Returns the stable stream-host config probe id.
29pub fn host_stream_config_probe_symbol() -> Symbol {
30    Symbol::qualified("config-probe", "stream-host")
31}
32
33/// Returns the stable capability id required for real hardware inventory probes.
34pub fn hardware_inventory_probe_capability_symbol() -> Symbol {
35    Symbol::qualified("config-probe-capability", "hardware-inventory")
36}
37
38/// Safe config probe for stream-host audio and MIDI defaults.
39pub struct HostStreamConfigProbe {
40    catalog: DeviceCatalog,
41}
42
43impl HostStreamConfigProbe {
44    /// Builds a probe over a caller-supplied device catalog.
45    pub fn new(catalog: DeviceCatalog) -> Self {
46        Self { catalog }
47    }
48
49    /// Builds a deterministic modeled probe for validation.
50    pub fn modeled() -> Self {
51        Self::new(DeviceCatalog::default_modeled())
52    }
53}
54
55impl Default for HostStreamConfigProbe {
56    fn default() -> Self {
57        Self::modeled()
58    }
59}
60
61impl ConfigProbe for HostStreamConfigProbe {
62    fn symbol(&self) -> Symbol {
63        host_stream_config_probe_symbol()
64    }
65
66    fn probe(&self, request: &ConfigProbeRequest) -> (Option<ConfigLayer>, ConfigProbeReport) {
67        if request.lib != stream_host_config_lib_symbol() {
68            return (
69                None,
70                report(
71                    self.symbol(),
72                    request,
73                    ConfigProbeStatus::Skipped {
74                        reason: "stream-host probe only serves stream/host".to_owned(),
75                    },
76                    &[],
77                ),
78            );
79        }
80
81        if request.mode == ProbeMode::Real && !request.caps.hardware_inventory {
82            return (
83                None,
84                report(
85                    self.symbol(),
86                    request,
87                    ConfigProbeStatus::Denied {
88                        capability: hardware_inventory_probe_capability_symbol().to_string(),
89                    },
90                    &[],
91                ),
92            );
93        }
94
95        let candidates = match request.mode {
96            ProbeMode::Modeled => Ok((vec!["modeled".to_owned()], vec!["modeled".to_owned()])),
97            ProbeMode::Real => self.real_candidates(),
98        };
99        let (audio_candidates, midi_candidates) = match candidates {
100            Ok(candidates) => candidates,
101            Err(message) => {
102                return (
103                    None,
104                    report(
105                        self.symbol(),
106                        request,
107                        ConfigProbeStatus::Failed { message },
108                        &[],
109                    ),
110                );
111            }
112        };
113
114        let layer = ConfigLayer::new(
115            ConfigSource::Probe {
116                probe: self.symbol(),
117                mode: request.mode,
118            },
119            ConfigDir::one(
120                request.lib.clone(),
121                stream_host_table(&audio_candidates, &midi_candidates),
122            )
123            .expect("stream-host config probe builds a map table"),
124        );
125        (
126            Some(layer),
127            report(
128                self.symbol(),
129                request,
130                ConfigProbeStatus::Applied,
131                &EMITTED_KEYS,
132            ),
133        )
134    }
135}
136
137impl HostStreamConfigProbe {
138    fn real_candidates(&self) -> Result<(Vec<String>, Vec<String>), String> {
139        let audio = self
140            .catalog
141            .audio_hardware_backend_names()
142            .map_err(|error| format!("audio backend inventory failed: {error}"))?;
143        let midi = self
144            .catalog
145            .midi_hardware_backend_names()
146            .map_err(|error| format!("MIDI backend inventory failed: {error}"))?;
147        Ok((audio, midi))
148    }
149}
150
151fn stream_host_table(audio_candidates: &[String], midi_candidates: &[String]) -> Expr {
152    Expr::Map(vec![
153        (
154            key("audio_backend_candidates"),
155            string_list(audio_candidates),
156        ),
157        (key("midi_backend_candidates"), string_list(midi_candidates)),
158        (
159            key("audio_backend_regex"),
160            Expr::String(candidate_regex(audio_candidates)),
161        ),
162        (
163            key("midi_backend_regex"),
164            Expr::String(candidate_regex(midi_candidates)),
165        ),
166        (key("sample_rate_hz"), int(DEFAULT_SAMPLE_RATE_HZ)),
167        (key("max_block_frames"), int(DEFAULT_MAX_BLOCK_FRAMES)),
168    ])
169}
170
171fn report(
172    probe: Symbol,
173    request: &ConfigProbeRequest,
174    status: ConfigProbeStatus,
175    keys: &[&str],
176) -> ConfigProbeReport {
177    ConfigProbeReport {
178        probe,
179        lib: request.lib.clone(),
180        mode: request.mode,
181        status,
182        emitted_keys: keys.iter().map(|key| (*key).to_owned()).collect(),
183    }
184}
185
186fn key(name: &str) -> Expr {
187    Expr::Symbol(Symbol::new(name))
188}
189
190fn string_list(values: &[String]) -> Expr {
191    Expr::List(
192        values
193            .iter()
194            .map(|value| Expr::String(value.clone()))
195            .collect(),
196    )
197}
198
199fn int(value: u32) -> Expr {
200    Expr::Number(NumberLiteral {
201        domain: Symbol::new("i64"),
202        canonical: value.to_string(),
203    })
204}
205
206fn candidate_regex(values: &[String]) -> String {
207    if values.is_empty() {
208        return "(?!)".to_owned();
209    }
210    format!(
211        "^(?:{})$",
212        values
213            .iter()
214            .map(|value| regex_escape(value))
215            .collect::<Vec<_>>()
216            .join("|")
217    )
218}
219
220fn regex_escape(value: &str) -> String {
221    let mut escaped = String::new();
222    for character in value.chars() {
223        if matches!(
224            character,
225            '\\' | '.' | '+' | '*' | '?' | '^' | '$' | '(' | ')' | '[' | ']' | '{' | '}' | '|'
226        ) {
227            escaped.push('\\');
228        }
229        escaped.push(character);
230    }
231    escaped
232}