1use reqwest::header::HeaderValue;
2use serde_json::Value as JsonValue;
3
4use supabase_client_core::SupabaseError;
5
6use crate::backend::QueryBackend;
7use crate::filter::Filterable;
8use crate::modifier::Modifiable;
9use crate::sql::{FilterCondition, ParamStore, SqlParts};
10
11pub struct GeoJsonSelectBuilder {
16 pub(crate) backend: QueryBackend,
17 pub(crate) parts: SqlParts,
18 pub(crate) params: ParamStore,
19}
20
21impl Filterable for GeoJsonSelectBuilder {
22 fn filters_mut(&mut self) -> &mut Vec<FilterCondition> {
23 &mut self.parts.filters
24 }
25 fn params_mut(&mut self) -> &mut ParamStore {
26 &mut self.params
27 }
28}
29
30impl Modifiable for GeoJsonSelectBuilder {
31 fn parts_mut(&mut self) -> &mut SqlParts {
32 &mut self.parts
33 }
34}
35
36impl GeoJsonSelectBuilder {
37 pub fn schema(mut self, schema: &str) -> Self {
39 self.parts.schema_override = Some(schema.to_string());
40 self
41 }
42
43 pub async fn execute(self) -> Result<JsonValue, SupabaseError> {
45 match &self.backend {
46 QueryBackend::Rest { http, base_url, api_key, schema } => {
47 let (url, mut headers) = crate::postgrest::build_postgrest_select(
48 base_url, &self.parts, &self.params,
49 )
50 .map_err(SupabaseError::QueryBuilder)?;
51
52 headers.insert(
54 "Accept",
55 HeaderValue::from_static("application/geo+json"),
56 );
57
58 headers.insert("apikey", HeaderValue::from_str(api_key).unwrap());
60 headers.insert(
61 "Authorization",
62 HeaderValue::from_str(&format!("Bearer {}", api_key)).unwrap(),
63 );
64
65 if let Some(ref so) = self.parts.schema_override {
67 headers.insert(
68 "Accept-Profile",
69 HeaderValue::from_str(so).unwrap(),
70 );
71 } else if schema != "public" {
72 headers.entry("Accept-Profile")
73 .or_insert_with(|| HeaderValue::from_str(schema).unwrap());
74 }
75
76 let response = http
77 .get(&url)
78 .headers(headers)
79 .send()
80 .await
81 .map_err(|e| SupabaseError::Http(e.to_string()))?;
82
83 let status = response.status().as_u16();
84 let body = response
85 .text()
86 .await
87 .map_err(|e| SupabaseError::Http(e.to_string()))?;
88
89 if status >= 400 {
90 return Err(SupabaseError::postgrest(status, body, None));
91 }
92
93 serde_json::from_str(&body).map_err(|e| {
94 SupabaseError::Serialization(format!(
95 "Failed to parse GeoJSON response: {}",
96 e
97 ))
98 })
99 }
100 #[cfg(feature = "direct-sql")]
101 QueryBackend::DirectSql { .. } => {
102 Err(SupabaseError::query_builder(
103 "GeoJSON output is only supported with the REST (PostgREST) backend",
104 ))
105 }
106 }
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use crate::sql::{SqlOperation, SqlParts, ParamStore};
114
115 #[test]
116 fn test_geojson_builder_modifiable() {
117 let mut builder = GeoJsonSelectBuilder {
118 backend: QueryBackend::Rest {
119 http: reqwest::Client::new(),
120 base_url: "http://localhost".into(),
121 api_key: "key".into(),
122 schema: "public".to_string(),
123 },
124 parts: SqlParts::new(SqlOperation::Select, "public", "cities"),
125 params: ParamStore::new(),
126 };
127 builder = builder.limit(10);
128 assert_eq!(builder.parts.limit, Some(10));
129 }
130
131 #[test]
132 fn test_geojson_builder_filterable() {
133 let builder = GeoJsonSelectBuilder {
134 backend: QueryBackend::Rest {
135 http: reqwest::Client::new(),
136 base_url: "http://localhost".into(),
137 api_key: "key".into(),
138 schema: "public".to_string(),
139 },
140 parts: SqlParts::new(SqlOperation::Select, "public", "cities"),
141 params: ParamStore::new(),
142 };
143 let builder = builder.eq("name", "Auckland");
144 assert_eq!(builder.parts.filters.len(), 1);
145 }
146
147 #[test]
148 fn test_geojson_accept_header() {
149 let parts = SqlParts::new(SqlOperation::Select, "public", "cities");
150 let params = ParamStore::new();
151 let (_, mut headers) = crate::postgrest::build_postgrest_select(
152 "http://localhost:64321", &parts, ¶ms,
153 ).unwrap();
154 headers.insert("Accept", HeaderValue::from_static("application/geo+json"));
156 assert_eq!(headers.get("Accept").unwrap(), "application/geo+json");
157 }
158
159 #[test]
160 fn test_geojson_schema_sets_override() {
161 let builder = GeoJsonSelectBuilder {
162 backend: QueryBackend::Rest {
163 http: reqwest::Client::new(),
164 base_url: "http://localhost".into(),
165 api_key: "key".into(),
166 schema: "public".to_string(),
167 },
168 parts: SqlParts::new(SqlOperation::Select, "public", "cities"),
169 params: ParamStore::new(),
170 };
171 let builder = builder.schema("geo_schema");
172 assert_eq!(builder.parts.schema_override.as_deref(), Some("geo_schema"));
173 }
174
175 #[tokio::test]
178 async fn test_geojson_execute_success() {
179 use wiremock::matchers::{method, path};
180 use wiremock::{Mock, MockServer, ResponseTemplate};
181
182 let mock_server = MockServer::start().await;
183 let geojson = serde_json::json!({
184 "type": "FeatureCollection",
185 "features": [
186 {
187 "type": "Feature",
188 "geometry": {
189 "type": "Point",
190 "coordinates": [174.7633, -36.8485]
191 },
192 "properties": {
193 "name": "Auckland"
194 }
195 }
196 ]
197 });
198 Mock::given(method("GET"))
199 .and(path("/rest/v1/cities"))
200 .respond_with(
201 ResponseTemplate::new(200)
202 .set_body_json(geojson.clone()),
203 )
204 .mount(&mock_server)
205 .await;
206
207 let builder = GeoJsonSelectBuilder {
208 backend: QueryBackend::Rest {
209 http: reqwest::Client::new(),
210 base_url: mock_server.uri().into(),
211 api_key: "test-key".into(),
212 schema: "public".to_string(),
213 },
214 parts: SqlParts::new(SqlOperation::Select, "public", "cities"),
215 params: ParamStore::new(),
216 };
217
218 let result = builder.execute().await;
219 assert!(result.is_ok());
220 let value = result.unwrap();
221 assert_eq!(value["type"], "FeatureCollection");
222 assert_eq!(value["features"][0]["properties"]["name"], "Auckland");
223 }
224
225 #[tokio::test]
226 async fn test_geojson_execute_error() {
227 use wiremock::matchers::{method, path};
228 use wiremock::{Mock, MockServer, ResponseTemplate};
229
230 let mock_server = MockServer::start().await;
231 Mock::given(method("GET"))
232 .and(path("/rest/v1/nonexistent"))
233 .respond_with(
234 ResponseTemplate::new(404)
235 .set_body_string("Relation not found"),
236 )
237 .mount(&mock_server)
238 .await;
239
240 let builder = GeoJsonSelectBuilder {
241 backend: QueryBackend::Rest {
242 http: reqwest::Client::new(),
243 base_url: mock_server.uri().into(),
244 api_key: "test-key".into(),
245 schema: "public".to_string(),
246 },
247 parts: SqlParts::new(SqlOperation::Select, "public", "nonexistent"),
248 params: ParamStore::new(),
249 };
250
251 let result = builder.execute().await;
252 assert!(result.is_err());
253 match result.unwrap_err() {
254 SupabaseError::PostgRest { status, .. } => {
255 assert_eq!(status, 404);
256 }
257 other => panic!("Expected PostgRest error, got {:?}", other),
258 }
259 }
260
261 #[tokio::test]
262 async fn test_geojson_execute_invalid_json_body() {
263 use wiremock::matchers::{method, path};
264 use wiremock::{Mock, MockServer, ResponseTemplate};
265
266 let mock_server = MockServer::start().await;
267 Mock::given(method("GET"))
268 .and(path("/rest/v1/cities"))
269 .respond_with(
270 ResponseTemplate::new(200)
271 .set_body_string("this is not json"),
272 )
273 .mount(&mock_server)
274 .await;
275
276 let builder = GeoJsonSelectBuilder {
277 backend: QueryBackend::Rest {
278 http: reqwest::Client::new(),
279 base_url: mock_server.uri().into(),
280 api_key: "test-key".into(),
281 schema: "public".to_string(),
282 },
283 parts: SqlParts::new(SqlOperation::Select, "public", "cities"),
284 params: ParamStore::new(),
285 };
286
287 let result = builder.execute().await;
288 assert!(result.is_err());
289 match result.unwrap_err() {
290 SupabaseError::Serialization(msg) => {
291 assert!(msg.contains("Failed to parse GeoJSON"));
292 }
293 other => panic!("Expected Serialization error, got {:?}", other),
294 }
295 }
296}