foxglove 0.25.3

Foxglove SDK
Documentation
//! Remote access implementation.

mod capability;
mod channel_registry;
mod client;
mod connection;
mod gateway;
mod listener;
mod parameter_subscriptions;
mod participant;
pub(super) mod protocol_version;
mod qos;
mod rtt_tracker;
pub mod service;
mod session;
mod sse;
mod watch;
mod watch_loop;

pub use crate::remote_common::{
    AnyClient, AssetHandler, AssetResponder, ClientId, ConnectionGraph, GetParametersResponder,
    Parameter, ParameterDecodeError, ParameterHandler, ParameterType, ParameterValue,
    SetParametersResponder, Status, StatusLevel,
};
pub use capability::Capability;
pub use client::Client;
pub use connection::ConnectionStatus;
pub use gateway::{Gateway, GatewayHandle};
pub use listener::Listener;
pub use qos::{QosClassifier, QosProfile, QosProfileBuilder, Reliability};

use reqwest::StatusCode;
use thiserror::Error;

use crate::api_client::FoxgloveApiClientError;

/// Internal error type for the remote access module.
#[derive(Error, Debug)]
pub(super) enum RemoteAccessError {
    /// An error from a LiveKit byte stream operation.
    #[error("Stream error: {0}")]
    Stream(livekit::StreamError),
    /// An error from a LiveKit room connection or operation.
    #[error("Room error: {0}")]
    Room(livekit::RoomError),
    /// A failed Foxglove API call.
    #[error("API error: {0}")]
    Api(FoxgloveApiClientError),
    /// An I/O error.
    #[error(transparent)]
    Io(#[from] std::io::Error),
}

impl RemoteAccessError {
    /// True if this is an [`Api`](Self::Api) error carrying a 401.
    pub(super) fn is_unauthorized(&self) -> bool {
        matches!(self, Self::Api(api) if api.status_code() == Some(StatusCode::UNAUTHORIZED))
    }
}

impl From<livekit::StreamError> for RemoteAccessError {
    fn from(error: livekit::StreamError) -> Self {
        match error {
            livekit::StreamError::Io(e) => RemoteAccessError::Io(e),
            other => RemoteAccessError::Stream(other),
        }
    }
}

impl From<livekit::RoomError> for RemoteAccessError {
    fn from(error: livekit::RoomError) -> Self {
        RemoteAccessError::Room(error)
    }
}

impl From<FoxgloveApiClientError> for RemoteAccessError {
    fn from(error: FoxgloveApiClientError) -> Self {
        RemoteAccessError::Api(error)
    }
}

impl From<livekit::StreamError> for Box<RemoteAccessError> {
    fn from(e: livekit::StreamError) -> Self {
        Box::new(RemoteAccessError::from(e))
    }
}

impl From<livekit::RoomError> for Box<RemoteAccessError> {
    fn from(e: livekit::RoomError) -> Self {
        Box::new(RemoteAccessError::from(e))
    }
}

impl From<FoxgloveApiClientError> for Box<RemoteAccessError> {
    fn from(e: FoxgloveApiClientError) -> Self {
        Box::new(RemoteAccessError::from(e))
    }
}

impl From<std::io::Error> for Box<RemoteAccessError> {
    fn from(e: std::io::Error) -> Self {
        Box::new(RemoteAccessError::from(e))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn stream_io_error_converts_to_io_variant() {
        let io_err = std::io::Error::new(std::io::ErrorKind::BrokenPipe, "broken");
        let stream_err = livekit::StreamError::Io(io_err);
        let err = RemoteAccessError::from(stream_err);
        assert!(
            matches!(err, RemoteAccessError::Io(_)),
            "StreamError::Io should convert to RemoteAccessError::Io"
        );
    }

    #[test]
    fn stream_non_io_error_converts_to_stream_variant() {
        let stream_err = livekit::StreamError::AlreadyClosed;
        let err = RemoteAccessError::from(stream_err);
        assert!(
            matches!(err, RemoteAccessError::Stream(_)),
            "Non-Io StreamError should convert to RemoteAccessError::Stream"
        );
    }

    #[test]
    fn room_error_converts_to_room_variant() {
        let room_err = livekit::RoomError::AlreadyClosed;
        let err = RemoteAccessError::from(room_err);
        assert!(
            matches!(err, RemoteAccessError::Room(_)),
            "RoomError should convert to RemoteAccessError::Room"
        );
    }

    #[test]
    fn io_error_converts_to_io_variant() {
        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "not found");
        let err = RemoteAccessError::from(io_err);
        assert!(
            matches!(err, RemoteAccessError::Io(_)),
            "io::Error should convert to RemoteAccessError::Io"
        );
    }

    /// Helper to produce a `FoxgloveApiClientError` by sending a request with a bad token
    /// to a mock server that returns 401.
    async fn make_api_client_error() -> FoxgloveApiClientError {
        use crate::api_client::test_utils::create_test_server;
        use crate::api_client::{DeviceToken, FoxgloveApiClientBuilder};
        let server = create_test_server().await;
        let client = FoxgloveApiClientBuilder::new(DeviceToken::new("bad-token"))
            .base_url(server.url())
            .build()
            .expect("client should build successfully");
        // `fetch_device_info` will fail with 401 because of the bad token.
        match client.fetch_device_info().await {
            Err(e) => e,
            Ok(_) => panic!("expected fetch_device_info to fail with bad token"),
        }
    }

    #[tokio::test]
    async fn api_client_error_converts_to_api_variant() {
        let err = RemoteAccessError::from(make_api_client_error().await);
        assert!(
            matches!(err, RemoteAccessError::Api(_)),
            "FoxgloveApiClientError should convert to RemoteAccessError::Api"
        );
    }
}