Skip to main content

sim_lib_view_device/
degrade.rs

1//! Degradation from requested profile to observed route capabilities.
2
3use crate::ladder::DeviceTier;
4use crate::profile::{DeviceProfile, DeviceProfileParts, derive_tier, has_symbol, push_existing};
5use sim_kernel::{Expr, Symbol};
6
7/// Capabilities observed on a concrete route and its attached accessories.
8#[derive(Clone, Debug, Default, PartialEq, Eq)]
9pub struct ObservedRoute {
10    /// Observed display capability tokens.
11    pub display: Vec<Symbol>,
12    /// Observed input capability tokens.
13    pub input: Vec<Symbol>,
14    /// Observed output capability tokens.
15    pub output: Vec<Symbol>,
16    /// Observed link tokens.
17    pub links: Vec<Symbol>,
18    /// Observed stream tokens.
19    pub streams: Vec<Symbol>,
20}
21
22impl ObservedRoute {
23    /// Builds an observed route from an existing profile.
24    pub fn from_profile(profile: &DeviceProfile) -> Self {
25        Self {
26            display: profile.display.clone(),
27            input: profile.input.clone(),
28            output: profile.output.clone(),
29            links: profile.links.clone(),
30            streams: profile.streams.clone(),
31        }
32    }
33}
34
35/// The resolved device tier and any unavailable requested capabilities.
36#[derive(Clone, Debug, PartialEq, Eq)]
37pub struct Degradation {
38    /// Highest tier supported by the observed route.
39    pub tier: DeviceTier,
40    /// Human-readable explanations for unavailable requested capabilities.
41    pub reasons: Vec<String>,
42}
43
44/// Resolves device degradation against observed route/accessory data.
45#[derive(Clone, Debug, Default)]
46pub struct DegradationResolver;
47
48impl DegradationResolver {
49    /// Maps the requested profile to the highest observed tier with reasons for
50    /// every unavailable requested capability.
51    pub fn resolve(requested: &DeviceProfile, observed: &ObservedRoute) -> Degradation {
52        let supported = DeviceProfile::new(DeviceProfileParts {
53            kind: requested.kind.clone(),
54            display: intersection(&requested.display, &observed.display),
55            input: intersection(&requested.input, &observed.input),
56            output: intersection(&requested.output, &observed.output),
57            links: intersection(&requested.links, &observed.links),
58            streams: intersection(&requested.streams, &observed.streams),
59            rate: requested.rate,
60            policy: Expr::Map(Vec::new()),
61        });
62        let mut reasons = Vec::new();
63        missing_reasons(
64            &mut reasons,
65            "display",
66            &requested.display,
67            &observed.display,
68        );
69        missing_reasons(&mut reasons, "input", &requested.input, &observed.input);
70        missing_reasons(&mut reasons, "output", &requested.output, &observed.output);
71        missing_reasons(&mut reasons, "link", &requested.links, &observed.links);
72        missing_reasons(
73            &mut reasons,
74            "stream",
75            &requested.streams,
76            &observed.streams,
77        );
78        Degradation {
79            tier: derive_tier(&supported),
80            reasons,
81        }
82    }
83
84    /// Returns a display-only degradation result with a single explanation.
85    pub fn unavailable(reason: impl Into<String>) -> Degradation {
86        Degradation {
87            tier: DeviceTier::Display,
88            reasons: vec![reason.into()],
89        }
90    }
91}
92
93fn intersection(requested: &[Symbol], observed: &[Symbol]) -> Vec<Symbol> {
94    let mut out = Vec::new();
95    for symbol in requested {
96        if observed.iter().any(|candidate| candidate == symbol) {
97            push_existing(&mut out, symbol.clone());
98        }
99    }
100    out
101}
102
103fn missing_reasons(
104    reasons: &mut Vec<String>,
105    lane: &str,
106    requested: &[Symbol],
107    observed: &[Symbol],
108) {
109    for symbol in requested {
110        if !has_symbol(observed, symbol.name.as_ref()) {
111            reasons.push(format!("missing {lane}: {}", symbol.as_qualified_str()));
112        }
113    }
114}