firebase_rs_sdk/firestore/remote/
connection.rs1use 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}