stac_client/models.rs
1//! STAC data models, such as `Item`, `Collection`, and `Catalog`.
2//!
3//! The structs in this module represent the core objects in the
4//! [STAC specification](https://stacspec.org/). They are designed to be
5//! deserialized from STAC API responses and provide access to STAC metadata.
6//!
7//! Fields that are not part of the core specification but are used by common
8//! extensions (like `datetime`) are included as optional fields. Any other
9//! non-standard fields are collected in an `extra` field to ensure forward
10//! compatibility.
11
12use chrono::{DateTime, Utc};
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15
16/// A type alias for an arbitrary JSON value.
17pub type JsonValue = serde_json::Value;
18/// A type alias for the `properties` field of a STAC `Item`.
19pub type Properties = HashMap<String, JsonValue>;
20
21/// Represents a STAC Link object.
22///
23/// See the [STAC Link Object specification](https://github.com/radiantearth/stac-spec/blob/v1.0.0/spec/item-spec/item-spec.md#link-object)
24/// for details.
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct Link {
27 /// The absolute or relative URL of the linked resource.
28 pub href: String,
29 /// The relation type of the link (e.g., `self`, `root`, `item`).
30 pub rel: String,
31 /// The media type of the linked resource.
32 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
33 pub media_type: Option<String>,
34 /// A human-readable title for the link.
35 #[serde(skip_serializing_if = "Option::is_none")]
36 pub title: Option<String>,
37 /// Additional fields not part of the core specification.
38 #[serde(flatten)]
39 pub extra: HashMap<String, JsonValue>,
40}
41
42/// Represents a STAC Asset object, which is a link to a resource.
43///
44/// See the [STAC Asset Object specification](https://github.com/radiantearth/stac-spec/blob/v1.0.0/spec/item-spec/item-spec.md#asset-object)
45/// for details.
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct Asset {
48 /// The URL of the asset.
49 pub href: String,
50 /// A human-readable title for the asset.
51 #[serde(skip_serializing_if = "Option::is_none")]
52 pub title: Option<String>,
53 /// A description of the asset.
54 #[serde(skip_serializing_if = "Option::is_none")]
55 pub description: Option<String>,
56 /// The media type of the asset.
57 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
58 pub media_type: Option<String>,
59 /// The semantic roles of the asset (e.g., `thumbnail`, `overview`).
60 #[serde(skip_serializing_if = "Option::is_none")]
61 pub roles: Option<Vec<String>>,
62 /// Additional fields not part of the core specification.
63 #[serde(flatten)]
64 pub extra: HashMap<String, JsonValue>,
65}
66
67/// Represents a STAC Provider object, which describes an organization providing data.
68///
69/// See the [STAC Provider Object specification](https://github.com/radiantearth/stac-spec/blob/v1.0.0/collection-spec/collection-spec.md#provider-object)
70/// for details.
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct Provider {
73 /// The name of the provider.
74 pub name: String,
75 /// A description of the provider.
76 #[serde(skip_serializing_if = "Option::is_none")]
77 pub description: Option<String>,
78 /// The roles of the provider (e.g., `producer`, `licensor`).
79 #[serde(skip_serializing_if = "Option::is_none")]
80 pub roles: Option<Vec<String>>,
81 /// A URL to the provider's website.
82 #[serde(skip_serializing_if = "Option::is_none")]
83 pub url: Option<String>,
84}
85
86/// Represents a STAC Extent, which describes the spatial and temporal range of a `Collection`.
87///
88/// See the [STAC Extent Object specification](https://github.com/radiantearth/stac-spec/blob/v1.0.0/collection-spec/collection-spec.md#extent-object)
89/// for details.
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct Extent {
92 /// The spatial extent (bounding box) of the collection.
93 pub spatial: SpatialExtent,
94 /// The temporal extent (time interval) of the collection.
95 pub temporal: TemporalExtent,
96}
97
98/// The spatial component of a STAC `Extent`.
99#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct SpatialExtent {
101 /// A list of bounding boxes covering the spatial extent.
102 pub bbox: Vec<Vec<f64>>,
103}
104
105/// The temporal component of a STAC `Extent`.
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct TemporalExtent {
108 /// A list of time intervals, where `None` indicates an open-ended interval.
109 pub interval: Vec<Vec<Option<DateTime<Utc>>>>,
110}
111
112/// Represents a STAC Catalog object.
113///
114/// See the [STAC Catalog Specification](https://github.com/radiantearth/stac-spec/blob/v1.0.0/catalog-spec/catalog-spec.md)
115/// for details.
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct Catalog {
118 /// The type of the object, which is always "Catalog".
119 #[serde(rename = "type")]
120 pub catalog_type: String,
121 /// The STAC version the catalog implements.
122 pub stac_version: String,
123 /// The ID of the catalog.
124 pub id: String,
125 /// A human-readable title for the catalog.
126 pub title: Option<String>,
127 /// A detailed description of the catalog.
128 pub description: String,
129 /// A list of links to other STAC objects.
130 pub links: Vec<Link>,
131 /// Additional fields not part of the core specification.
132 #[serde(flatten)]
133 pub extra: HashMap<String, JsonValue>,
134}
135
136/// Represents a STAC Collection object.
137///
138/// See the [STAC Collection Specification](https://github.com/radiantearth/stac-spec/blob/v1.0.0/collection-spec/collection-spec.md)
139/// for details.
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct Collection {
142 /// The type of the object, which is always "Collection".
143 #[serde(rename = "type")]
144 pub collection_type: String,
145 /// The STAC version the collection implements.
146 pub stac_version: String,
147 /// The ID of the collection.
148 pub id: String,
149 /// A human-readable title for the collection.
150 pub title: Option<String>,
151 /// A detailed description of the collection.
152 pub description: String,
153 /// A list of keywords describing the collection.
154 pub keywords: Option<Vec<String>>,
155 /// The license of the data in the collection.
156 pub license: String,
157 /// A list of providers for the collection.
158 pub providers: Option<Vec<Provider>>,
159 /// The spatial and temporal extent of the collection.
160 pub extent: Extent,
161 /// A map of summaries of item properties.
162 pub summaries: Option<HashMap<String, JsonValue>>,
163 /// A list of links to other STAC objects.
164 pub links: Vec<Link>,
165 /// A map of assets available at the collection level.
166 pub assets: Option<HashMap<String, Asset>>,
167 /// Additional fields not part of the core specification.
168 #[serde(flatten)]
169 pub extra: HashMap<String, JsonValue>,
170}
171
172/// Represents a STAC Item object, which is a `GeoJSON` Feature.
173///
174/// See the [STAC Item Specification](https://github.com/radiantearth/stac-spec/blob/v1.0.0/item-spec/item-spec.md)
175/// for details.
176#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct Item {
178 /// The type of the object, which is always "Feature".
179 #[serde(rename = "type")]
180 pub item_type: String,
181 /// The STAC version the item implements.
182 pub stac_version: String,
183 /// The ID of the item.
184 pub id: String,
185 /// The `GeoJSON` geometry of the item.
186 pub geometry: Option<JsonValue>,
187 /// The bounding box of the item's geometry.
188 pub bbox: Option<Vec<f64>>,
189 /// A dictionary of metadata properties.
190 pub properties: Properties,
191 /// A list of links to other STAC objects.
192 pub links: Vec<Link>,
193 /// A map of assets, such as data files.
194 pub assets: HashMap<String, Asset>,
195 /// The ID of the collection this item belongs to.
196 pub collection: Option<String>,
197 /// Additional fields not part of the core specification.
198 #[serde(flatten)]
199 pub extra: HashMap<String, JsonValue>,
200}
201
202/// Represents a `GeoJSON` `FeatureCollection` containing STAC `Item` objects.
203///
204/// This is the standard response format for a STAC API search. See the
205/// [STAC API ItemCollection docs](https://github.com/radiantearth/stac-api-spec/blob/v1.0.0/fragments/itemcollection/README.md)
206/// for details.
207#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct ItemCollection {
209 /// The type of the object, which is always "`FeatureCollection`".
210 #[serde(rename = "type")]
211 pub collection_type: String,
212 /// The list of `Item` objects returned by the search.
213 pub features: Vec<Item>,
214 /// A list of links related to the search results, such as for pagination.
215 pub links: Option<Vec<Link>>,
216 /// Context metadata about the search results.
217 pub context: Option<SearchContext>,
218}
219
220/// Context metadata for an `ItemCollection` response from a STAC API search.
221///
222/// See the [STAC API Context Object docs](https://github.com/radiantearth/stac-api-spec/blob/v1.0.0/fragments/context/README.md)
223/// for details.
224#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct SearchContext {
226 /// The number of items returned in the current response.
227 pub returned: u64,
228 /// The maximum number of items requested.
229 pub limit: Option<u64>,
230 /// The total number of items that matched the search criteria.
231 pub matched: Option<u64>,
232}
233
234/// Parameters for a STAC API search.
235///
236/// This struct is used with `SearchBuilder` to construct a query for the
237/// `POST /search` or `GET /search` endpoints.
238#[derive(Debug, Clone, Default, Serialize)]
239pub struct SearchParams {
240 /// The maximum number of items to return.
241 #[serde(skip_serializing_if = "Option::is_none")]
242 pub limit: Option<u32>,
243 /// A bounding box to filter items by.
244 #[serde(skip_serializing_if = "Option::is_none")]
245 pub bbox: Option<Vec<f64>>,
246 /// A datetime string or interval to filter items by.
247 #[serde(skip_serializing_if = "Option::is_none")]
248 pub datetime: Option<String>,
249 /// A `GeoJSON` geometry to filter items by spatial intersection.
250 #[serde(skip_serializing_if = "Option::is_none")]
251 pub intersects: Option<JsonValue>,
252 /// A list of collection IDs to search within.
253 #[serde(skip_serializing_if = "Option::is_none")]
254 pub collections: Option<Vec<String>>,
255 /// A list of item IDs to retrieve.
256 #[serde(skip_serializing_if = "Option::is_none")]
257 pub ids: Option<Vec<String>>,
258 /// A map of query expressions for filtering item properties.
259 #[serde(skip_serializing_if = "Option::is_none")]
260 pub query: Option<HashMap<String, JsonValue>>,
261 /// A list of sorting rules.
262 #[serde(skip_serializing_if = "Option::is_none")]
263 pub sortby: Option<Vec<SortBy>>,
264 /// A filter for including or excluding specific fields.
265 #[serde(skip_serializing_if = "Option::is_none")]
266 pub fields: Option<FieldsFilter>,
267}
268
269/// A sorting rule for a STAC API search.
270///
271/// See the [STAC API Sort Extension](https://github.com/radiantearth/stac-api-spec/tree/v1.0.0/fragments/sort)
272/// for details.
273#[derive(Debug, Clone, Serialize)]
274pub struct SortBy {
275 /// The property name to sort by.
276 pub field: String,
277 /// The sort direction.
278 pub direction: SortDirection,
279}
280
281/// The direction for a sort rule.
282#[derive(Debug, Clone, Serialize)]
283#[serde(rename_all = "lowercase")]
284pub enum SortDirection {
285 /// Sort in ascending order.
286 Asc,
287 /// Sort in descending order.
288 Desc,
289}
290
291/// A filter for including or excluding fields in a STAC API search response.
292///
293/// See the [STAC API Fields Extension](https://github.com/radiantearth/stac-api-spec/tree/v1.0.0/fragments/fields)
294/// for details.
295#[derive(Debug, Clone, Default, Serialize)]
296pub struct FieldsFilter {
297 /// A list of fields to include in the response.
298 #[serde(skip_serializing_if = "Option::is_none")]
299 pub include: Option<Vec<String>>,
300 /// A list of fields to exclude from the response.
301 #[serde(skip_serializing_if = "Option::is_none")]
302 pub exclude: Option<Vec<String>>,
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308 use serde_json::json;
309
310 #[test]
311 fn test_catalog_serialization() {
312 let catalog = Catalog {
313 catalog_type: "Catalog".to_string(),
314 stac_version: "1.0.0".to_string(),
315 id: "test-catalog".to_string(),
316 title: Some("Test Catalog".to_string()),
317 description: "A test catalog".to_string(),
318 links: vec![],
319 extra: HashMap::new(),
320 };
321
322 let json_str = serde_json::to_string(&catalog).unwrap();
323 let deserialized: Catalog = serde_json::from_str(&json_str).unwrap();
324
325 assert_eq!(catalog.id, deserialized.id);
326 assert_eq!(catalog.stac_version, deserialized.stac_version);
327 }
328
329 #[test]
330 fn test_collection_serialization() {
331 let collection = Collection {
332 collection_type: "Collection".to_string(),
333 stac_version: "1.0.0".to_string(),
334 id: "test-collection".to_string(),
335 title: Some("Test Collection".to_string()),
336 description: "A test collection".to_string(),
337 keywords: Some(vec!["test".to_string()]),
338 license: "MIT".to_string(),
339 providers: None,
340 extent: Extent {
341 spatial: SpatialExtent {
342 bbox: vec![vec![-180.0, -90.0, 180.0, 90.0]],
343 },
344 temporal: TemporalExtent {
345 interval: vec![vec![None, None]],
346 },
347 },
348 summaries: None,
349 links: vec![],
350 assets: None,
351 extra: HashMap::new(),
352 };
353
354 let json_str = serde_json::to_string(&collection).unwrap();
355 let deserialized: Collection = serde_json::from_str(&json_str).unwrap();
356
357 assert_eq!(collection.id, deserialized.id);
358 assert_eq!(collection.license, deserialized.license);
359 }
360
361 #[test]
362 fn test_item_serialization() {
363 let mut properties = HashMap::new();
364 properties.insert("datetime".to_string(), json!("2023-01-01T12:00:00Z"));
365
366 let item = Item {
367 item_type: "Feature".to_string(),
368 stac_version: "1.0.0".to_string(),
369 id: "test-item".to_string(),
370 geometry: None,
371 bbox: Some(vec![-180.0, -90.0, 180.0, 90.0]),
372 properties,
373 links: vec![],
374 assets: HashMap::new(),
375 collection: Some("test-collection".to_string()),
376 extra: HashMap::new(),
377 };
378
379 let json_str = serde_json::to_string(&item).unwrap();
380 let deserialized: Item = serde_json::from_str(&json_str).unwrap();
381
382 assert_eq!(item.id, deserialized.id);
383 assert_eq!(item.collection, deserialized.collection);
384 }
385
386 #[test]
387 fn test_search_params_default() {
388 let params = SearchParams::default();
389 assert!(params.limit.is_none());
390 assert!(params.bbox.is_none());
391 assert!(params.collections.is_none());
392 }
393
394 #[test]
395 fn test_sort_direction_serialization() {
396 let asc = SortDirection::Asc;
397 let desc = SortDirection::Desc;
398
399 assert_eq!(serde_json::to_string(&asc).unwrap(), "\"asc\"");
400 assert_eq!(serde_json::to_string(&desc).unwrap(), "\"desc\"");
401 }
402}