firebase_rs_sdk/data_connect/
config.rs

1use serde::{Deserialize, Serialize};
2use std::fmt::{Display, Formatter};
3
4use crate::data_connect::error::{invalid_argument, DataConnectResult};
5
6/// Default production host for the Data Connect REST API.
7pub const DEFAULT_DATA_CONNECT_HOST: &str = "firebasedataconnect.googleapis.com";
8
9/// Root connector configuration (location/connector/service) supplied by the user.
10#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)]
11pub struct ConnectorConfig {
12    pub location: String,
13    pub connector: String,
14    pub service: String,
15}
16
17impl ConnectorConfig {
18    /// Validates and constructs a new connector configuration.
19    pub fn new(
20        location: impl Into<String>,
21        connector: impl Into<String>,
22        service: impl Into<String>,
23    ) -> DataConnectResult<Self> {
24        let config = Self {
25            location: location.into(),
26            connector: connector.into(),
27            service: service.into(),
28        };
29        config.validate()?;
30        Ok(config)
31    }
32
33    /// Ensures mandatory fields are present.
34    pub fn validate(&self) -> DataConnectResult<()> {
35        if self.location.trim().is_empty() {
36            return Err(invalid_argument("location is required"));
37        }
38        if self.connector.trim().is_empty() {
39            return Err(invalid_argument("connector is required"));
40        }
41        if self.service.trim().is_empty() {
42            return Err(invalid_argument("service is required"));
43        }
44        Ok(())
45    }
46
47    /// Stable string identifier used as the component instance key.
48    pub fn identifier(&self) -> String {
49        serde_json::to_string(self).expect("connector config serialization")
50    }
51}
52
53impl Display for ConnectorConfig {
54    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
55        write!(f, "{}/{}/{}", self.location, self.service, self.connector)
56    }
57}
58
59/// Fully-qualified options passed to the transport layer once the project ID is known.
60#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
61pub struct DataConnectOptions {
62    pub connector: ConnectorConfig,
63    pub project_id: String,
64}
65
66impl DataConnectOptions {
67    pub fn new(
68        connector: ConnectorConfig,
69        project_id: impl Into<String>,
70    ) -> DataConnectResult<Self> {
71        let options = Self {
72            connector,
73            project_id: project_id.into(),
74        };
75        options.connector.validate()?;
76        if options.project_id.trim().is_empty() {
77            return Err(invalid_argument("project_id is required"));
78        }
79        Ok(options)
80    }
81
82    /// Returns the canonical resource path for this connector (without the API host).
83    pub fn resource_path(&self) -> String {
84        format!(
85            "projects/{}/locations/{}/services/{}/connectors/{}",
86            self.project_id,
87            self.connector.location,
88            self.connector.service,
89            self.connector.connector
90        )
91    }
92}
93
94/// Host/port/SSL tuple used for emulator connections.
95#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
96pub struct TransportOptions {
97    pub host: String,
98    pub port: Option<u16>,
99    pub ssl_enabled: bool,
100}
101
102impl TransportOptions {
103    pub fn new(host: impl Into<String>, port: Option<u16>, ssl_enabled: bool) -> Self {
104        Self {
105            host: host.into(),
106            port,
107            ssl_enabled,
108        }
109    }
110
111    /// Builds the base URL given the configured host/port.
112    pub fn base_url(&self) -> String {
113        let scheme = if self.ssl_enabled { "https" } else { "http" };
114        match self.port {
115            Some(port) => format!("{scheme}://{}:{port}", self.host),
116            None => format!("{scheme}://{}", self.host),
117        }
118    }
119}
120
121impl Default for TransportOptions {
122    fn default() -> Self {
123        Self {
124            host: DEFAULT_DATA_CONNECT_HOST.to_string(),
125            port: None,
126            ssl_enabled: true,
127        }
128    }
129}
130
131/// Parses the `FIREBASE_DATA_CONNECT_EMULATOR_HOST` environment variable payload.
132pub fn parse_transport_options(spec: &str) -> DataConnectResult<TransportOptions> {
133    let (protocol, rest) = spec.split_once("://").unwrap_or(("https", spec));
134    let ssl_enabled = match protocol {
135        "http" => false,
136        "https" => true,
137        other => {
138            return Err(invalid_argument(format!(
139                "Unsupported protocol '{other}' in emulator host"
140            )))
141        }
142    };
143
144    let (host, port) = if let Some((host, port)) = rest.split_once(':') {
145        let port = port
146            .parse::<u16>()
147            .map_err(|_| invalid_argument("Port must be a number in emulator host declaration"))?;
148        (host.to_string(), Some(port))
149    } else {
150        (rest.to_string(), None)
151    };
152
153    if host.trim().is_empty() {
154        return Err(invalid_argument(
155            "Host is required for emulator connections",
156        ));
157    }
158
159    Ok(TransportOptions::new(host, port, ssl_enabled))
160}