firebase_rs_sdk/firestore/remote/
connection.rs

1use std::time::Duration;
2
3use reqwest::blocking::{Client, RequestBuilder};
4use reqwest::{Method, StatusCode};
5use serde_json::Value as JsonValue;
6
7use crate::firestore::error::{internal_error, FirestoreResult};
8use crate::firestore::model::DatabaseId;
9
10use super::rpc_error::map_http_error;
11
12const FIRESTORE_API_HOST: &str = "https://firestore.googleapis.com";
13const FIRESTORE_API_VERSION: &str = "v1";
14
15#[derive(Clone, Debug)]
16pub struct Connection {
17    client: Client,
18    base_url: String,
19}
20
21#[derive(Clone, Debug)]
22pub struct ConnectionBuilder {
23    database_id: DatabaseId,
24    client: Option<Client>,
25    emulator_host: Option<String>,
26}
27
28#[derive(Default, Clone, Debug)]
29pub struct RequestContext {
30    pub auth_token: Option<String>,
31    pub app_check_token: Option<String>,
32    pub request_timeout: Option<Duration>,
33}
34
35impl ConnectionBuilder {
36    pub fn new(database_id: DatabaseId) -> Self {
37        Self {
38            database_id,
39            client: None,
40            emulator_host: std::env::var("FIRESTORE_EMULATOR_HOST").ok(),
41        }
42    }
43
44    pub fn with_client(mut self, client: Client) -> Self {
45        self.client = Some(client);
46        self
47    }
48
49    pub fn with_emulator_host(mut self, host: impl Into<String>) -> Self {
50        self.emulator_host = Some(host.into());
51        self
52    }
53
54    pub fn build(self) -> FirestoreResult<Connection> {
55        let client = match self.client {
56            Some(client) => client,
57            None => Client::builder()
58                .build()
59                .map_err(|err| internal_error(err.to_string()))?,
60        };
61        let base_url = build_base_url(&self.database_id, self.emulator_host.as_deref());
62        Ok(Connection { client, base_url })
63    }
64}
65
66impl Connection {
67    pub fn builder(database_id: DatabaseId) -> ConnectionBuilder {
68        ConnectionBuilder::new(database_id)
69    }
70
71    pub fn base_url(&self) -> &str {
72        &self.base_url
73    }
74
75    pub fn invoke_json(
76        &self,
77        method: Method,
78        path: &str,
79        body: Option<JsonValue>,
80        context: &RequestContext,
81    ) -> FirestoreResult<JsonValue> {
82        let mut request = self.build_request(method, path, context);
83        if let Some(body) = body {
84            request = request.json(&body);
85        }
86        let response = request
87            .send()
88            .map_err(|err| internal_error(err.to_string()))?;
89        let status = response.status();
90        let text = response
91            .text()
92            .map_err(|err| internal_error(err.to_string()))?;
93        if status.is_success() {
94            if text.is_empty() {
95                Ok(JsonValue::Null)
96            } else {
97                serde_json::from_str(&text).map_err(|err| internal_error(err.to_string()))
98            }
99        } else {
100            Err(map_http_error(status, &text))
101        }
102    }
103
104    pub fn invoke_json_optional(
105        &self,
106        method: Method,
107        path: &str,
108        body: Option<JsonValue>,
109        context: &RequestContext,
110    ) -> FirestoreResult<Option<JsonValue>> {
111        let mut request = self.build_request(method, path, context);
112        if let Some(body) = body {
113            request = request.json(&body);
114        }
115        let response = request
116            .send()
117            .map_err(|err| internal_error(err.to_string()))?;
118        let status = response.status();
119        let text = response
120            .text()
121            .map_err(|err| internal_error(err.to_string()))?;
122        if status.is_success() {
123            if text.is_empty() {
124                Ok(Some(JsonValue::Null))
125            } else {
126                serde_json::from_str(&text)
127                    .map(Some)
128                    .map_err(|err| internal_error(err.to_string()))
129            }
130        } else if status == StatusCode::NOT_FOUND {
131            Ok(None)
132        } else {
133            Err(map_http_error(status, &text))
134        }
135    }
136
137    fn build_request(
138        &self,
139        method: Method,
140        path: &str,
141        context: &RequestContext,
142    ) -> RequestBuilder {
143        let url = format!("{}/{}", self.base_url, path.trim_start_matches('/'));
144        let mut builder = self.client.request(method, url);
145        if let Some(timeout) = context.request_timeout {
146            builder = builder.timeout(timeout);
147        }
148        if let Some(token) = context.auth_token.as_deref() {
149            builder = builder.bearer_auth(token);
150        }
151        if let Some(app_check) = context.app_check_token.as_deref() {
152            builder = builder.header("X-Firebase-AppCheck", app_check);
153        }
154        builder = builder.header("Content-Type", "application/json");
155        builder
156    }
157}
158
159fn build_base_url(database_id: &DatabaseId, emulator_host: Option<&str>) -> String {
160    match emulator_host {
161        Some(host) => format!(
162            "http://{host}/{api_version}/projects/{}/databases/{}",
163            database_id.project_id(),
164            database_id.database(),
165            api_version = FIRESTORE_API_VERSION
166        ),
167        None => format!(
168            "{host}/{api_version}/projects/{}/databases/{}",
169            database_id.project_id(),
170            database_id.database(),
171            host = FIRESTORE_API_HOST,
172            api_version = FIRESTORE_API_VERSION
173        ),
174    }
175}