Skip to main content

oxigdal_stac/
builder.rs

1//! Builder patterns for STAC objects.
2//!
3//! This module provides fluent builder APIs for creating STAC Catalogs,
4//! Collections, and Items.
5
6use crate::{
7    asset::Asset,
8    catalog::Catalog,
9    collection::{Collection, Provider},
10    error::Result,
11    item::{Item, Link},
12};
13use chrono::{DateTime, Utc};
14
15/// Builder for creating STAC Catalogs.
16#[derive(Debug, Clone)]
17pub struct CatalogBuilder {
18    catalog: Catalog,
19}
20
21impl CatalogBuilder {
22    /// Creates a new CatalogBuilder.
23    ///
24    /// # Arguments
25    ///
26    /// * `id` - Unique identifier for the catalog
27    /// * `description` - Description of the catalog
28    ///
29    /// # Returns
30    ///
31    /// A new CatalogBuilder instance
32    pub fn new(id: impl Into<String>, description: impl Into<String>) -> Self {
33        Self {
34            catalog: Catalog::new(id, description),
35        }
36    }
37
38    /// Sets the title of the catalog.
39    ///
40    /// # Arguments
41    ///
42    /// * `title` - Title for the catalog
43    ///
44    /// # Returns
45    ///
46    /// Self for method chaining
47    pub fn title(mut self, title: impl Into<String>) -> Self {
48        self.catalog = self.catalog.with_title(title);
49        self
50    }
51
52    /// Adds a link to the catalog.
53    ///
54    /// # Arguments
55    ///
56    /// * `href` - URI to the linked resource
57    /// * `rel` - Relationship type
58    ///
59    /// # Returns
60    ///
61    /// Self for method chaining
62    pub fn link(mut self, href: impl Into<String>, rel: impl Into<String>) -> Self {
63        self.catalog = self.catalog.add_link(Link::new(href, rel));
64        self
65    }
66
67    /// Adds a self link to the catalog.
68    ///
69    /// # Arguments
70    ///
71    /// * `href` - URI to self
72    ///
73    /// # Returns
74    ///
75    /// Self for method chaining
76    pub fn self_link(self, href: impl Into<String>) -> Self {
77        self.link(href, "self")
78    }
79
80    /// Adds a root link to the catalog.
81    ///
82    /// # Arguments
83    ///
84    /// * `href` - URI to root catalog
85    ///
86    /// # Returns
87    ///
88    /// Self for method chaining
89    pub fn root_link(self, href: impl Into<String>) -> Self {
90        self.link(href, "root")
91    }
92
93    /// Adds a child link to the catalog.
94    ///
95    /// # Arguments
96    ///
97    /// * `href` - URI to child catalog or collection
98    ///
99    /// # Returns
100    ///
101    /// Self for method chaining
102    pub fn child_link(self, href: impl Into<String>) -> Self {
103        self.link(href, "child")
104    }
105
106    /// Adds an extension to the catalog.
107    ///
108    /// # Arguments
109    ///
110    /// * `extension` - Extension schema URI
111    ///
112    /// # Returns
113    ///
114    /// Self for method chaining
115    pub fn extension(mut self, extension: impl Into<String>) -> Self {
116        self.catalog = self.catalog.add_extension(extension);
117        self
118    }
119
120    /// Builds the catalog.
121    ///
122    /// # Returns
123    ///
124    /// The constructed Catalog
125    pub fn build(self) -> Result<Catalog> {
126        self.catalog.validate()?;
127        Ok(self.catalog)
128    }
129}
130
131/// Builder for creating STAC Collections.
132#[derive(Debug, Clone)]
133pub struct CollectionBuilder {
134    collection: Collection,
135}
136
137impl CollectionBuilder {
138    /// Creates a new CollectionBuilder.
139    ///
140    /// # Arguments
141    ///
142    /// * `id` - Unique identifier for the collection
143    /// * `description` - Description of the collection
144    /// * `license` - License identifier or URL
145    ///
146    /// # Returns
147    ///
148    /// A new CollectionBuilder instance
149    pub fn new(
150        id: impl Into<String>,
151        description: impl Into<String>,
152        license: impl Into<String>,
153    ) -> Self {
154        Self {
155            collection: Collection::new(id, description, license),
156        }
157    }
158
159    /// Sets the title of the collection.
160    ///
161    /// # Arguments
162    ///
163    /// * `title` - Title for the collection
164    ///
165    /// # Returns
166    ///
167    /// Self for method chaining
168    pub fn title(mut self, title: impl Into<String>) -> Self {
169        self.collection = self.collection.with_title(title);
170        self
171    }
172
173    /// Sets the keywords of the collection.
174    ///
175    /// # Arguments
176    ///
177    /// * `keywords` - Vector of keywords
178    ///
179    /// # Returns
180    ///
181    /// Self for method chaining
182    pub fn keywords(mut self, keywords: Vec<String>) -> Self {
183        self.collection = self.collection.with_keywords(keywords);
184        self
185    }
186
187    /// Adds a provider to the collection.
188    ///
189    /// # Arguments
190    ///
191    /// * `name` - Provider name
192    ///
193    /// # Returns
194    ///
195    /// Self for method chaining
196    pub fn provider(mut self, name: impl Into<String>) -> Self {
197        self.collection = self.collection.add_provider(Provider::new(name));
198        self
199    }
200
201    /// Sets the spatial extent of the collection.
202    ///
203    /// # Arguments
204    ///
205    /// * `west` - Western longitude
206    /// * `south` - Southern latitude
207    /// * `east` - Eastern longitude
208    /// * `north` - Northern latitude
209    ///
210    /// # Returns
211    ///
212    /// Self for method chaining
213    pub fn spatial_extent(mut self, west: f64, south: f64, east: f64, north: f64) -> Self {
214        self.collection = self
215            .collection
216            .with_spatial_extent(vec![west, south, east, north]);
217        self
218    }
219
220    /// Sets the temporal extent of the collection.
221    ///
222    /// # Arguments
223    ///
224    /// * `start` - Start datetime (None for open start)
225    /// * `end` - End datetime (None for open end)
226    ///
227    /// # Returns
228    ///
229    /// Self for method chaining
230    pub fn temporal_extent(
231        mut self,
232        start: Option<DateTime<Utc>>,
233        end: Option<DateTime<Utc>>,
234    ) -> Self {
235        self.collection = self.collection.with_temporal_extent(start, end);
236        self
237    }
238
239    /// Adds a link to the collection.
240    ///
241    /// # Arguments
242    ///
243    /// * `href` - URI to the linked resource
244    /// * `rel` - Relationship type
245    ///
246    /// # Returns
247    ///
248    /// Self for method chaining
249    pub fn link(mut self, href: impl Into<String>, rel: impl Into<String>) -> Self {
250        self.collection = self.collection.add_link(Link::new(href, rel));
251        self
252    }
253
254    /// Adds a self link to the collection.
255    ///
256    /// # Arguments
257    ///
258    /// * `href` - URI to self
259    ///
260    /// # Returns
261    ///
262    /// Self for method chaining
263    pub fn self_link(self, href: impl Into<String>) -> Self {
264        self.link(href, "self")
265    }
266
267    /// Adds an extension to the collection.
268    ///
269    /// # Arguments
270    ///
271    /// * `extension` - Extension schema URI
272    ///
273    /// # Returns
274    ///
275    /// Self for method chaining
276    pub fn extension(mut self, extension: impl Into<String>) -> Self {
277        self.collection = self.collection.add_extension(extension);
278        self
279    }
280
281    /// Builds the collection.
282    ///
283    /// # Returns
284    ///
285    /// The constructed Collection
286    pub fn build(self) -> Result<Collection> {
287        self.collection.validate()?;
288        Ok(self.collection)
289    }
290}
291
292/// Builder for creating STAC Items.
293#[derive(Debug, Clone)]
294pub struct ItemBuilder {
295    item: Item,
296}
297
298impl ItemBuilder {
299    /// Creates a new ItemBuilder.
300    ///
301    /// # Arguments
302    ///
303    /// * `id` - Unique identifier for the item
304    ///
305    /// # Returns
306    ///
307    /// A new ItemBuilder instance
308    pub fn new(id: impl Into<String>) -> Self {
309        Self {
310            item: Item::new(id),
311        }
312    }
313
314    /// Sets the geometry of the item.
315    ///
316    /// # Arguments
317    ///
318    /// * `geometry` - GeoJSON geometry
319    ///
320    /// # Returns
321    ///
322    /// Self for method chaining
323    pub fn geometry(mut self, geometry: geojson::Geometry) -> Self {
324        self.item = self.item.with_geometry(geometry);
325        self
326    }
327
328    /// Sets the bounding box of the item.
329    ///
330    /// # Arguments
331    ///
332    /// * `west` - Western longitude
333    /// * `south` - Southern latitude
334    /// * `east` - Eastern longitude
335    /// * `north` - Northern latitude
336    ///
337    /// # Returns
338    ///
339    /// Self for method chaining
340    pub fn bbox(mut self, west: f64, south: f64, east: f64, north: f64) -> Self {
341        self.item = self.item.with_bbox(vec![west, south, east, north]);
342        self
343    }
344
345    /// Sets the datetime of the item.
346    ///
347    /// # Arguments
348    ///
349    /// * `datetime` - Date and time in UTC
350    ///
351    /// # Returns
352    ///
353    /// Self for method chaining
354    pub fn datetime(mut self, datetime: DateTime<Utc>) -> Self {
355        self.item = self.item.with_datetime(datetime);
356        self
357    }
358
359    /// Sets the datetime range of the item.
360    ///
361    /// # Arguments
362    ///
363    /// * `start` - Start date and time in UTC
364    /// * `end` - End date and time in UTC
365    ///
366    /// # Returns
367    ///
368    /// Self for method chaining
369    pub fn datetime_range(mut self, start: DateTime<Utc>, end: DateTime<Utc>) -> Self {
370        self.item = self.item.with_datetime_range(start, end);
371        self
372    }
373
374    /// Adds an asset to the item.
375    ///
376    /// # Arguments
377    ///
378    /// * `key` - Asset key
379    /// * `asset` - Asset to add
380    ///
381    /// # Returns
382    ///
383    /// Self for method chaining
384    pub fn asset(mut self, key: impl Into<String>, asset: Asset) -> Self {
385        self.item = self.item.add_asset(key, asset);
386        self
387    }
388
389    /// Adds a simple asset with just an href.
390    ///
391    /// # Arguments
392    ///
393    /// * `key` - Asset key
394    /// * `href` - URI to the asset
395    ///
396    /// # Returns
397    ///
398    /// Self for method chaining
399    pub fn simple_asset(self, key: impl Into<String>, href: impl Into<String>) -> Self {
400        self.asset(key, Asset::new(href))
401    }
402
403    /// Adds a link to the item.
404    ///
405    /// # Arguments
406    ///
407    /// * `href` - URI to the linked resource
408    /// * `rel` - Relationship type
409    ///
410    /// # Returns
411    ///
412    /// Self for method chaining
413    pub fn link(mut self, href: impl Into<String>, rel: impl Into<String>) -> Self {
414        self.item = self.item.add_link(Link::new(href, rel));
415        self
416    }
417
418    /// Sets the collection ID.
419    ///
420    /// # Arguments
421    ///
422    /// * `collection_id` - Collection identifier
423    ///
424    /// # Returns
425    ///
426    /// Self for method chaining
427    pub fn collection(mut self, collection_id: impl Into<String>) -> Self {
428        self.item = self.item.with_collection(collection_id);
429        self
430    }
431
432    /// Adds an extension to the item.
433    ///
434    /// # Arguments
435    ///
436    /// * `extension` - Extension schema URI
437    ///
438    /// # Returns
439    ///
440    /// Self for method chaining
441    pub fn extension(mut self, extension: impl Into<String>) -> Self {
442        self.item = self.item.add_extension(extension);
443        self
444    }
445
446    /// Sets a property value.
447    ///
448    /// # Arguments
449    ///
450    /// * `key` - Property key
451    /// * `value` - Property value
452    ///
453    /// # Returns
454    ///
455    /// Self for method chaining
456    pub fn property(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
457        self.item
458            .properties
459            .additional_fields
460            .insert(key.into(), value);
461        self
462    }
463
464    /// Builds the item.
465    ///
466    /// # Returns
467    ///
468    /// The constructed Item
469    pub fn build(self) -> Result<Item> {
470        self.item.validate()?;
471        Ok(self.item)
472    }
473}
474
475#[cfg(test)]
476mod tests {
477    use super::*;
478
479    #[test]
480    fn test_catalog_builder() {
481        let catalog = CatalogBuilder::new("test-catalog", "A test catalog")
482            .title("Test Catalog")
483            .self_link("https://example.com/catalog.json")
484            .child_link("https://example.com/collection.json")
485            .build();
486
487        assert!(catalog.is_ok());
488        let catalog = catalog.expect("Failed to build catalog");
489        assert_eq!(catalog.id, "test-catalog");
490        assert_eq!(catalog.title, Some("Test Catalog".to_string()));
491        assert_eq!(catalog.links.len(), 2);
492    }
493
494    #[test]
495    fn test_collection_builder() {
496        let now = Utc::now();
497        let collection = CollectionBuilder::new("test-collection", "A test collection", "MIT")
498            .title("Test Collection")
499            .keywords(vec!["test".to_string(), "example".to_string()])
500            .provider("Test Provider")
501            .spatial_extent(-180.0, -90.0, 180.0, 90.0)
502            .temporal_extent(Some(now), None)
503            .build();
504
505        assert!(collection.is_ok());
506        let collection = collection.expect("Failed to build collection");
507        assert_eq!(collection.id, "test-collection");
508        assert_eq!(collection.title, Some("Test Collection".to_string()));
509    }
510
511    #[test]
512    fn test_item_builder() {
513        let now = Utc::now();
514        let geometry = geojson::Geometry::new_point([-122.0, 37.0]);
515
516        let item = ItemBuilder::new("test-item")
517            .geometry(geometry)
518            .bbox(-122.5, 36.5, -121.5, 37.5)
519            .datetime(now)
520            .simple_asset("data", "https://example.com/data.tif")
521            .collection("test-collection")
522            .build();
523
524        assert!(item.is_ok());
525        let item = item.expect("Failed to build item");
526        assert_eq!(item.id, "test-item");
527        assert_eq!(item.assets.len(), 1);
528        assert_eq!(item.collection, Some("test-collection".to_string()));
529    }
530
531    #[test]
532    fn test_item_builder_with_properties() {
533        let now = Utc::now();
534
535        let item = ItemBuilder::new("test-item")
536            .datetime(now)
537            .property("cloud_cover", serde_json::json!(10.5))
538            .property("platform", serde_json::json!("sentinel-2a"))
539            .build();
540
541        assert!(item.is_ok());
542        let item = item.expect("Failed to build item");
543        assert_eq!(
544            item.properties.additional_fields.get("cloud_cover"),
545            Some(&serde_json::json!(10.5))
546        );
547    }
548}