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