Skip to main content

lzc_sdk/
service_status.rs

1use std::collections::{HashMap, HashSet};
2use std::fmt;
3use std::sync::Arc;
4
5use tonic::Code;
6use tonic::transport::Channel;
7
8use crate::proto::common::file_handler_server;
9use crate::proto::localdevice::device_service_client::DeviceServiceClient;
10use crate::proto::localdevice::photo_library_server;
11use crate::proto::localdevice::{LocalServiceState, LocalServiceStatus, QueryServiceStatusRequest};
12use crate::{AuthenticatedService, Error};
13
14type DeviceServiceTransport = AuthenticatedService<Channel>;
15
16/// Public service availability state used across device API versions.
17#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
18pub enum ServiceState {
19    #[default]
20    Unknown,
21    Available,
22    Unavailable,
23}
24
25impl ServiceState {
26    /// Return the Go-compatible lowercase state name.
27    #[must_use]
28    pub const fn as_str(self) -> &'static str {
29        match self {
30            Self::Unknown => "unknown",
31            Self::Available => "available",
32            Self::Unavailable => "unavailable",
33        }
34    }
35}
36
37impl fmt::Display for ServiceState {
38    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
39        formatter.write_str(self.as_str())
40    }
41}
42
43/// Availability and optional device-provided reason for a local service.
44#[derive(Clone, Debug, Default, Eq, PartialEq)]
45pub struct ServiceStatus {
46    pub state: ServiceState,
47    pub reason: String,
48}
49
50/// Query helper bound to one generated service name.
51#[derive(Clone, Debug)]
52pub struct ServiceStatusQuerier {
53    registry: ServiceStatusRegistry,
54    service_name: Arc<str>,
55}
56
57impl ServiceStatusQuerier {
58    fn new(registry: ServiceStatusRegistry, service_name: &'static str) -> Self {
59        Self {
60            registry,
61            service_name: service_name.into(),
62        }
63    }
64
65    /// Query this helper's bound service.
66    ///
67    /// # Errors
68    ///
69    /// Returns a typed unsupported error or the underlying gRPC status.
70    pub async fn query(&self) -> Result<ServiceStatus, Error> {
71        self.registry.query(self.service_name.as_ref()).await
72    }
73}
74
75/// Query helper for arbitrary local service names.
76#[derive(Clone, Debug)]
77pub struct ServiceStatusRegistry {
78    service: DeviceServiceTransport,
79}
80
81impl ServiceStatusRegistry {
82    pub(crate) fn new(service: DeviceServiceTransport) -> Self {
83        Self { service }
84    }
85
86    /// Query one service, returning `Unknown` when the device omits it.
87    ///
88    /// # Errors
89    ///
90    /// Returns a typed unsupported error or the underlying gRPC status.
91    pub async fn query(&self, service_name: &str) -> Result<ServiceStatus, Error> {
92        let mut statuses = self.query_many([service_name]).await?;
93        Ok(statuses.remove(service_name).unwrap_or_default())
94    }
95
96    /// Query several services in one RPC after removing duplicate names.
97    ///
98    /// # Errors
99    ///
100    /// Returns a typed unsupported error or the underlying gRPC status.
101    pub async fn query_many<I, S>(
102        &self,
103        service_names: I,
104    ) -> Result<HashMap<String, ServiceStatus>, Error>
105    where
106        I: IntoIterator<Item = S>,
107        S: AsRef<str>,
108    {
109        let mut seen = HashSet::new();
110        let service_name = service_names
111            .into_iter()
112            .filter_map(|name| {
113                let name = name.as_ref();
114                if seen.insert(name.to_owned()) {
115                    Some(name.to_owned())
116                } else {
117                    None
118                }
119            })
120            .collect();
121        let response = DeviceServiceClient::new(self.service.clone())
122            .query_service_status(QueryServiceStatusRequest { service_name })
123            .await
124            .map_err(|status| {
125                if status.code() == Code::Unimplemented {
126                    Error::ServiceStatusUnsupported
127                } else {
128                    Error::GrpcStatus(status)
129                }
130            })?
131            .into_inner();
132        Ok(response
133            .services
134            .into_iter()
135            .map(|(name, status)| (name, convert_status(status)))
136            .collect())
137    }
138}
139
140fn convert_status(status: LocalServiceStatus) -> ServiceStatus {
141    let state = match LocalServiceState::try_from(status.state).unwrap_or_default() {
142        LocalServiceState::Available => ServiceState::Available,
143        LocalServiceState::Unavailable => ServiceState::Unavailable,
144        LocalServiceState::Unknown => ServiceState::Unknown,
145    };
146    ServiceStatus {
147        state,
148        reason: status.reason,
149    }
150}
151
152/// Named status helpers exposed by a device proxy.
153#[derive(Clone, Debug)]
154pub struct DeviceProxyStatus {
155    photo_library: ServiceStatusQuerier,
156    file_handler: ServiceStatusQuerier,
157    services: ServiceStatusRegistry,
158}
159
160impl DeviceProxyStatus {
161    pub(crate) fn new(service: DeviceServiceTransport) -> Self {
162        let services = ServiceStatusRegistry::new(service);
163        Self {
164            photo_library: ServiceStatusQuerier::new(
165                services.clone(),
166                photo_library_server::SERVICE_NAME,
167            ),
168            file_handler: ServiceStatusQuerier::new(
169                services.clone(),
170                file_handler_server::SERVICE_NAME,
171            ),
172            services,
173        }
174    }
175
176    #[must_use]
177    pub fn photo_library(&self) -> ServiceStatusQuerier {
178        self.photo_library.clone()
179    }
180
181    #[must_use]
182    pub fn file_handler(&self) -> ServiceStatusQuerier {
183        self.file_handler.clone()
184    }
185
186    #[must_use]
187    pub fn services(&self) -> ServiceStatusRegistry {
188        self.services.clone()
189    }
190}