//! # `chanora_bridge` //! //! Typed Flutter/Rust bridge — schema-controlled DTOs for commands, //! results, and events. Backed by `flutter_rust_bridge` 2.x per //! DEC-014. //! //! ## Alpha scope //! //! Exposes three commands that target the Alpha release goal: //! //! * `bridge_init()` — one-time process initialisation. Sets up //! logging. //! * `connect(host, nickname)` — connects to a TS3-compatible //! server and returns a typed [`ConnectResultDto`] containing the //! server snapshot. //! * `disconnect()` — clean disconnect. //! * `snapshot()` — re-fetch the current server snapshot. //! //! No audio commands cross the bridge in Alpha; audio is Beta scope. //! //! ## Boundary discipline //! //! Every type in this module is `Serialize + Deserialize` over owned //! primitives or `String`s. No `tsclientlib`, `cpal`, or backend //! types may appear in the public surface (SAD-067, SDD-079). //! //! Note: this crate cannot use `#![forbid(unsafe_code)]` because the //! FRB-generated glue (in `frb_generated`) legitimately uses unsafe //! for the FFI boundary. Hand-written code in this crate must //! nonetheless avoid `unsafe` and is held to that standard by review. #![warn(missing_docs)] pub mod api; mod frb_generated; #[cfg(target_os = "android")] mod android_init; use thiserror::Error; /// Errors raised at the bridge boundary. Production code must keep /// these user-safe — no secrets, no protocol details, no path /// information beyond what the redaction policy permits. #[derive(Debug, Error, Clone, serde::Serialize, serde::Deserialize)] pub enum BridgeError { /// The caller submitted a malformed command DTO. #[error("invalid command: {0}")] InvalidCommand(String), /// Hostname resolution failed. Distinct from `Connection` so the /// UI can show a meaningful "Server not found" message. #[error("dns: could not resolve '{host}': {reason}")] DnsFailed { /// The hostname (or `host:port`) the caller submitted. host: String, /// Reason from the platform resolver. reason: String, }, /// Connection layer failure (typed-mapped from CoreError). #[error("connection: {0}")] Connection(String), /// Operation requires an active connection. #[error("not connected")] NotConnected, /// Already connected; DEC-006 forbids a second concurrent /// connection. #[error("already connected")] AlreadyConnected, /// A server command was rejected by the TeamSpeak server. The /// `code` is the canonical TS3 error number (see /// https://github.com/ReSpeak/tsdeclarations Errors.csv); the /// `message` is the server-supplied text. The UI uses `code` /// to look up a localised explanation. #[error("server rejected (code {code}): {message}")] ServerRejected { /// Raw TS3 error code. code: u32, /// Server-supplied message text. message: String, }, /// An unmapped error escaped the subsystem boundary. Production /// callers should never see this; if they do, it is a mapping /// bug here. #[error("unmapped: {0}")] Unmapped(String), } impl From for BridgeError { fn from(e: chanora_core::CoreError) -> Self { match e { chanora_core::CoreError::NotConnected => BridgeError::NotConnected, chanora_core::CoreError::AlreadyConnected => BridgeError::AlreadyConnected, chanora_core::CoreError::AudioNotStarted => { BridgeError::InvalidCommand("audio not started".to_string()) } chanora_core::CoreError::Protocol(chanora_protocol::ProtocolError::DnsFailed { host, reason, }) => BridgeError::DnsFailed { host, reason }, chanora_core::CoreError::Protocol( chanora_protocol::ProtocolError::ServerRejected { code, message }, ) => BridgeError::ServerRejected { code, message }, chanora_core::CoreError::Protocol(p) => BridgeError::Connection(format!("{p}")), chanora_core::CoreError::Audio(a) => BridgeError::Connection(format!("audio: {a}")), chanora_core::CoreError::Storage(s) => BridgeError::Connection(format!("storage: {s}")), other => BridgeError::Unmapped(format!("{other}")), } } }