1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
10pub struct Link {
11 pub rel: String,
13 pub href: String,
15 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
17 pub link_type: Option<String>,
18 #[serde(skip_serializing_if = "Option::is_none")]
20 pub title: Option<String>,
21}
22
23impl Link {
24 pub fn new(rel: impl Into<String>, href: impl Into<String>) -> Self {
26 Self {
27 rel: rel.into(),
28 href: href.into(),
29 link_type: None,
30 title: None,
31 }
32 }
33
34 pub fn with_type(mut self, media_type: impl Into<String>) -> Self {
36 self.link_type = Some(media_type.into());
37 self
38 }
39
40 pub fn with_title(mut self, title: impl Into<String>) -> Self {
42 self.title = Some(title.into());
43 self
44 }
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
49#[serde(rename_all = "lowercase")]
50pub enum SortDirection {
51 Asc,
53 Desc,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
59pub struct SortField {
60 pub field: String,
62 pub direction: SortDirection,
64}
65
66impl SortField {
67 pub fn new(field: impl Into<String>, direction: SortDirection) -> Self {
69 Self {
70 field: field.into(),
71 direction,
72 }
73 }
74}
75
76#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
78pub struct FieldsSpec {
79 #[serde(default, skip_serializing_if = "Vec::is_empty")]
81 pub include: Vec<String>,
82 #[serde(default, skip_serializing_if = "Vec::is_empty")]
84 pub exclude: Vec<String>,
85}
86
87impl FieldsSpec {
88 pub fn new() -> Self {
90 Self::default()
91 }
92
93 pub fn include(mut self, field: impl Into<String>) -> Self {
95 self.include.push(field.into());
96 self
97 }
98
99 pub fn exclude(mut self, field: impl Into<String>) -> Self {
101 self.exclude.push(field.into());
102 self
103 }
104}
105
106#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
111pub struct SearchRequest {
112 #[serde(skip_serializing_if = "Option::is_none")]
114 pub bbox: Option<[f64; 4]>,
115
116 #[serde(skip_serializing_if = "Option::is_none")]
118 pub datetime: Option<String>,
119
120 #[serde(skip_serializing_if = "Option::is_none")]
122 pub filter: Option<serde_json::Value>,
123
124 #[serde(rename = "filter-lang", skip_serializing_if = "Option::is_none")]
126 pub filter_lang: Option<String>,
127
128 #[serde(default, skip_serializing_if = "Vec::is_empty")]
130 pub collections: Vec<String>,
131
132 #[serde(default, skip_serializing_if = "Vec::is_empty")]
134 pub ids: Vec<String>,
135
136 #[serde(skip_serializing_if = "Option::is_none")]
138 pub limit: Option<u32>,
139
140 #[serde(skip_serializing_if = "Option::is_none")]
142 pub token: Option<String>,
143
144 #[serde(default, skip_serializing_if = "Vec::is_empty")]
146 pub sortby: Vec<SortField>,
147
148 #[serde(skip_serializing_if = "Option::is_none")]
150 pub fields: Option<FieldsSpec>,
151}
152
153impl SearchRequest {
154 pub fn new() -> Self {
156 Self::default()
157 }
158
159 pub fn with_bbox(mut self, bbox: [f64; 4]) -> Self {
161 self.bbox = Some(bbox);
162 self
163 }
164
165 pub fn with_datetime(mut self, dt: impl Into<String>) -> Self {
167 self.datetime = Some(dt.into());
168 self
169 }
170
171 pub fn with_collections(mut self, cols: impl IntoIterator<Item = impl Into<String>>) -> Self {
173 self.collections = cols.into_iter().map(Into::into).collect();
174 self
175 }
176
177 pub fn with_ids(mut self, ids: impl IntoIterator<Item = impl Into<String>>) -> Self {
179 self.ids = ids.into_iter().map(Into::into).collect();
180 self
181 }
182
183 pub fn with_limit(mut self, n: u32) -> Self {
185 self.limit = Some(n);
186 self
187 }
188
189 pub fn with_sort(mut self, field: impl Into<String>, dir: SortDirection) -> Self {
191 self.sortby.push(SortField::new(field, dir));
192 self
193 }
194
195 pub fn with_filter(mut self, filter: serde_json::Value) -> Self {
197 self.filter = Some(filter);
198 self.filter_lang = Some("cql2-json".to_string());
199 self
200 }
201
202 pub fn with_fields(mut self, fields: FieldsSpec) -> Self {
204 self.fields = Some(fields);
205 self
206 }
207
208 pub fn with_token(mut self, token: impl Into<String>) -> Self {
210 self.token = Some(token.into());
211 self
212 }
213}
214
215#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
217pub struct SearchContext {
218 pub returned: u64,
220 #[serde(skip_serializing_if = "Option::is_none")]
222 pub limit: Option<u32>,
223 #[serde(skip_serializing_if = "Option::is_none")]
225 pub matched: Option<u64>,
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
230pub struct ItemCollection {
231 #[serde(rename = "type")]
233 pub collection_type: String,
234
235 pub features: Vec<serde_json::Value>,
237
238 #[serde(skip_serializing_if = "Option::is_none")]
240 pub links: Option<Vec<Link>>,
241
242 #[serde(skip_serializing_if = "Option::is_none")]
244 pub context: Option<SearchContext>,
245
246 #[serde(rename = "numberMatched", skip_serializing_if = "Option::is_none")]
248 pub number_matched: Option<u64>,
249
250 #[serde(rename = "numberReturned", skip_serializing_if = "Option::is_none")]
252 pub number_returned: Option<u64>,
253}
254
255impl ItemCollection {
256 pub fn new(features: Vec<serde_json::Value>) -> Self {
258 let n = features.len() as u64;
259 Self {
260 collection_type: "FeatureCollection".to_string(),
261 features,
262 links: None,
263 context: None,
264 number_matched: None,
265 number_returned: Some(n),
266 }
267 }
268
269 pub fn next_link(&self) -> Option<&Link> {
271 self.links
272 .as_ref()
273 .and_then(|ls| ls.iter().find(|l| l.rel == "next"))
274 }
275
276 pub fn has_next_page(&self) -> bool {
278 self.next_link().is_some()
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285
286 #[test]
287 fn test_search_request_default() {
288 let req = SearchRequest::new();
289 assert!(req.bbox.is_none());
290 assert!(req.collections.is_empty());
291 }
292
293 #[test]
294 fn test_with_bbox() {
295 let req = SearchRequest::new().with_bbox([-180.0, -90.0, 180.0, 90.0]);
296 assert_eq!(req.bbox, Some([-180.0, -90.0, 180.0, 90.0]));
297 }
298
299 #[test]
300 fn test_with_collections() {
301 let req = SearchRequest::new().with_collections(["sentinel-2-l2a"]);
302 assert_eq!(req.collections, vec!["sentinel-2-l2a"]);
303 }
304
305 #[test]
306 fn test_with_sort() {
307 let req = SearchRequest::new().with_sort("datetime", SortDirection::Desc);
308 assert_eq!(req.sortby.len(), 1);
309 assert_eq!(req.sortby[0].direction, SortDirection::Desc);
310 }
311
312 #[test]
313 fn test_search_request_roundtrip() {
314 let req = SearchRequest::new()
315 .with_bbox([-10.0, -10.0, 10.0, 10.0])
316 .with_datetime("2023-01-01/2024-01-01")
317 .with_collections(["my-collection"])
318 .with_limit(50);
319 let json = serde_json::to_string(&req).expect("serialize");
320 let back: SearchRequest = serde_json::from_str(&json).expect("deserialize");
321 assert_eq!(req, back);
322 }
323
324 #[test]
325 fn test_item_collection_new() {
326 let fc = ItemCollection::new(vec![]);
327 assert_eq!(fc.collection_type, "FeatureCollection");
328 assert_eq!(fc.number_returned, Some(0));
329 }
330
331 #[test]
332 fn test_has_next_page() {
333 let mut fc = ItemCollection::new(vec![]);
334 assert!(!fc.has_next_page());
335 fc.links = Some(vec![Link::new("next", "https://example.com?token=abc")]);
336 assert!(fc.has_next_page());
337 }
338
339 #[test]
340 fn test_fields_spec() {
341 let fs = FieldsSpec::new()
342 .include("properties.datetime")
343 .exclude("assets");
344 assert_eq!(fs.include, vec!["properties.datetime"]);
345 assert_eq!(fs.exclude, vec!["assets"]);
346 }
347}