Skip to main content

stationxml_rs/
builder.rs

1//! Builder pattern API for constructing inventories.
2//!
3//! Provides a fluent, closure-based API for building an [`Inventory`]
4//! without manually constructing every nested struct.
5//!
6//! # Example
7//!
8//! ```
9//! use stationxml_rs::Inventory;
10//!
11//! let inv = Inventory::builder()
12//!     .source("Pena Bumi")
13//!     .network("XX", |net| {
14//!         net.description("Local Test Network")
15//!            .station("PBUMI", |sta| {
16//!                sta.latitude(-7.7714)
17//!                   .longitude(110.3776)
18//!                   .elevation(150.0)
19//!                   .site_name("Yogyakarta")
20//!                   .channel("SHZ", "00", |ch| {
21//!                       ch.azimuth(0.0).dip(-90.0).sample_rate(100.0)
22//!                   })
23//!            })
24//!     })
25//!     .build();
26//!
27//! assert_eq!(inv.networks[0].stations[0].channels[0].code, "SHZ");
28//! ```
29
30use chrono::{DateTime, Utc};
31
32use crate::inventory::*;
33
34// ─── InventoryBuilder ───────────────────────────────────────────────
35
36/// Builder for [`Inventory`].
37pub struct InventoryBuilder {
38    source: String,
39    sender: Option<String>,
40    created: Option<DateTime<Utc>>,
41    networks: Vec<Network>,
42}
43
44impl Inventory {
45    /// Create a new inventory builder.
46    pub fn builder() -> InventoryBuilder {
47        InventoryBuilder {
48            source: String::new(),
49            sender: None,
50            created: None,
51            networks: vec![],
52        }
53    }
54}
55
56impl InventoryBuilder {
57    /// Set the source organization.
58    pub fn source(mut self, source: impl Into<String>) -> Self {
59        self.source = source.into();
60        self
61    }
62
63    /// Set the sender identifier.
64    pub fn sender(mut self, sender: impl Into<String>) -> Self {
65        self.sender = Some(sender.into());
66        self
67    }
68
69    /// Set the creation timestamp.
70    pub fn created(mut self, created: DateTime<Utc>) -> Self {
71        self.created = Some(created);
72        self
73    }
74
75    /// Add a network using a closure-based builder.
76    pub fn network(
77        mut self,
78        code: impl Into<String>,
79        f: impl FnOnce(NetworkBuilder) -> NetworkBuilder,
80    ) -> Self {
81        let builder = f(NetworkBuilder::new(code));
82        self.networks.push(builder.build());
83        self
84    }
85
86    /// Build the final [`Inventory`].
87    pub fn build(self) -> Inventory {
88        Inventory {
89            source: self.source,
90            sender: self.sender,
91            created: self.created,
92            networks: self.networks,
93        }
94    }
95}
96
97// ─── NetworkBuilder ─────────────────────────────────────────────────
98
99/// Builder for [`Network`].
100pub struct NetworkBuilder {
101    code: String,
102    description: Option<String>,
103    start_date: Option<DateTime<Utc>>,
104    end_date: Option<DateTime<Utc>>,
105    stations: Vec<Station>,
106}
107
108impl NetworkBuilder {
109    fn new(code: impl Into<String>) -> Self {
110        Self {
111            code: code.into(),
112            description: None,
113            start_date: None,
114            end_date: None,
115            stations: vec![],
116        }
117    }
118
119    /// Set the network description.
120    pub fn description(mut self, desc: impl Into<String>) -> Self {
121        self.description = Some(desc.into());
122        self
123    }
124
125    /// Set the start date.
126    pub fn start_date(mut self, date: DateTime<Utc>) -> Self {
127        self.start_date = Some(date);
128        self
129    }
130
131    /// Set the end date.
132    pub fn end_date(mut self, date: DateTime<Utc>) -> Self {
133        self.end_date = Some(date);
134        self
135    }
136
137    /// Add a station using a closure-based builder.
138    pub fn station(
139        mut self,
140        code: impl Into<String>,
141        f: impl FnOnce(StationBuilder) -> StationBuilder,
142    ) -> Self {
143        let builder = f(StationBuilder::new(code));
144        self.stations.push(builder.build());
145        self
146    }
147
148    fn build(self) -> Network {
149        Network {
150            code: self.code,
151            description: self.description,
152            start_date: self.start_date,
153            end_date: self.end_date,
154            stations: self.stations,
155        }
156    }
157}
158
159// ─── StationBuilder ─────────────────────────────────────────────────
160
161/// Builder for [`Station`].
162pub struct StationBuilder {
163    code: String,
164    description: Option<String>,
165    latitude: f64,
166    longitude: f64,
167    elevation: f64,
168    site_name: String,
169    start_date: Option<DateTime<Utc>>,
170    end_date: Option<DateTime<Utc>>,
171    creation_date: Option<DateTime<Utc>>,
172    channels: Vec<Channel>,
173}
174
175impl StationBuilder {
176    fn new(code: impl Into<String>) -> Self {
177        Self {
178            code: code.into(),
179            description: None,
180            latitude: 0.0,
181            longitude: 0.0,
182            elevation: 0.0,
183            site_name: String::new(),
184            start_date: None,
185            end_date: None,
186            creation_date: None,
187            channels: vec![],
188        }
189    }
190
191    pub fn latitude(mut self, lat: f64) -> Self {
192        self.latitude = lat;
193        self
194    }
195
196    pub fn longitude(mut self, lon: f64) -> Self {
197        self.longitude = lon;
198        self
199    }
200
201    pub fn elevation(mut self, elev: f64) -> Self {
202        self.elevation = elev;
203        self
204    }
205
206    pub fn site_name(mut self, name: impl Into<String>) -> Self {
207        self.site_name = name.into();
208        self
209    }
210
211    pub fn description(mut self, desc: impl Into<String>) -> Self {
212        self.description = Some(desc.into());
213        self
214    }
215
216    pub fn start_date(mut self, date: DateTime<Utc>) -> Self {
217        self.start_date = Some(date);
218        self
219    }
220
221    pub fn end_date(mut self, date: DateTime<Utc>) -> Self {
222        self.end_date = Some(date);
223        self
224    }
225
226    /// Add a channel using a closure-based builder.
227    ///
228    /// Channel lat/lon/elevation default to the station's values if not set.
229    pub fn channel(
230        mut self,
231        code: impl Into<String>,
232        location_code: impl Into<String>,
233        f: impl FnOnce(ChannelBuilder) -> ChannelBuilder,
234    ) -> Self {
235        let builder = f(ChannelBuilder::new(
236            code,
237            location_code,
238            self.latitude,
239            self.longitude,
240            self.elevation,
241        ));
242        self.channels.push(builder.build());
243        self
244    }
245
246    fn build(self) -> Station {
247        Station {
248            code: self.code,
249            description: self.description,
250            latitude: self.latitude,
251            longitude: self.longitude,
252            elevation: self.elevation,
253            site: Site {
254                name: self.site_name,
255                ..Default::default()
256            },
257            start_date: self.start_date,
258            end_date: self.end_date,
259            creation_date: self.creation_date,
260            channels: self.channels,
261        }
262    }
263}
264
265// ─── ChannelBuilder ─────────────────────────────────────────────────
266
267/// Builder for [`Channel`].
268pub struct ChannelBuilder {
269    code: String,
270    location_code: String,
271    latitude: f64,
272    longitude: f64,
273    elevation: f64,
274    depth: f64,
275    azimuth: f64,
276    dip: f64,
277    sample_rate: f64,
278    start_date: Option<DateTime<Utc>>,
279    end_date: Option<DateTime<Utc>>,
280    sensor: Option<Equipment>,
281    data_logger: Option<Equipment>,
282    response: Option<Response>,
283}
284
285impl ChannelBuilder {
286    fn new(
287        code: impl Into<String>,
288        location_code: impl Into<String>,
289        station_lat: f64,
290        station_lon: f64,
291        station_elev: f64,
292    ) -> Self {
293        Self {
294            code: code.into(),
295            location_code: location_code.into(),
296            latitude: station_lat,
297            longitude: station_lon,
298            elevation: station_elev,
299            depth: 0.0,
300            azimuth: 0.0,
301            dip: 0.0,
302            sample_rate: 0.0,
303            start_date: None,
304            end_date: None,
305            sensor: None,
306            data_logger: None,
307            response: None,
308        }
309    }
310
311    pub fn latitude(mut self, lat: f64) -> Self {
312        self.latitude = lat;
313        self
314    }
315
316    pub fn longitude(mut self, lon: f64) -> Self {
317        self.longitude = lon;
318        self
319    }
320
321    pub fn elevation(mut self, elev: f64) -> Self {
322        self.elevation = elev;
323        self
324    }
325
326    pub fn depth(mut self, depth: f64) -> Self {
327        self.depth = depth;
328        self
329    }
330
331    pub fn azimuth(mut self, azimuth: f64) -> Self {
332        self.azimuth = azimuth;
333        self
334    }
335
336    pub fn dip(mut self, dip: f64) -> Self {
337        self.dip = dip;
338        self
339    }
340
341    pub fn sample_rate(mut self, rate: f64) -> Self {
342        self.sample_rate = rate;
343        self
344    }
345
346    pub fn start_date(mut self, date: DateTime<Utc>) -> Self {
347        self.start_date = Some(date);
348        self
349    }
350
351    pub fn end_date(mut self, date: DateTime<Utc>) -> Self {
352        self.end_date = Some(date);
353        self
354    }
355
356    pub fn sensor(mut self, sensor: Equipment) -> Self {
357        self.sensor = Some(sensor);
358        self
359    }
360
361    pub fn data_logger(mut self, dl: Equipment) -> Self {
362        self.data_logger = Some(dl);
363        self
364    }
365
366    pub fn response(mut self, response: Response) -> Self {
367        self.response = Some(response);
368        self
369    }
370
371    fn build(self) -> Channel {
372        Channel {
373            code: self.code,
374            location_code: self.location_code,
375            latitude: self.latitude,
376            longitude: self.longitude,
377            elevation: self.elevation,
378            depth: self.depth,
379            azimuth: self.azimuth,
380            dip: self.dip,
381            sample_rate: self.sample_rate,
382            start_date: self.start_date,
383            end_date: self.end_date,
384            sensor: self.sensor,
385            data_logger: self.data_logger,
386            response: self.response,
387        }
388    }
389}
390
391#[cfg(test)]
392mod tests {
393    use super::*;
394
395    #[test]
396    fn builder_minimal() {
397        let inv = Inventory::builder().source("Test").build();
398        assert_eq!(inv.source, "Test");
399        assert!(inv.networks.is_empty());
400    }
401
402    #[test]
403    fn builder_full() {
404        let inv = Inventory::builder()
405            .source("Pena Bumi")
406            .sender("stationxml-rs")
407            .network("XX", |net| {
408                net.description("Local Test Network")
409                    .station("PBUMI", |sta| {
410                        sta.latitude(-7.7714)
411                            .longitude(110.3776)
412                            .elevation(150.0)
413                            .site_name("Yogyakarta Seismic Shelter")
414                            .channel("SHZ", "00", |ch| {
415                                ch.azimuth(0.0).dip(-90.0).sample_rate(100.0)
416                            })
417                            .channel("SHN", "00", |ch| {
418                                ch.azimuth(0.0).dip(0.0).sample_rate(100.0)
419                            })
420                            .channel("SHE", "00", |ch| {
421                                ch.azimuth(90.0).dip(0.0).sample_rate(100.0)
422                            })
423                    })
424            })
425            .build();
426
427        assert_eq!(inv.source, "Pena Bumi");
428        assert_eq!(inv.networks.len(), 1);
429        assert_eq!(inv.networks[0].code, "XX");
430
431        let sta = &inv.networks[0].stations[0];
432        assert_eq!(sta.code, "PBUMI");
433        assert_eq!(sta.channels.len(), 3);
434        assert_eq!(sta.site.name, "Yogyakarta Seismic Shelter");
435
436        // Channel inherits station coordinates
437        let shz = &sta.channels[0];
438        assert_eq!(shz.code, "SHZ");
439        assert_eq!(shz.latitude, sta.latitude);
440        assert_eq!(shz.longitude, sta.longitude);
441        assert_eq!(shz.dip, -90.0);
442
443        let she = &sta.channels[2];
444        assert_eq!(she.code, "SHE");
445        assert_eq!(she.azimuth, 90.0);
446        assert_eq!(she.dip, 0.0);
447    }
448
449    #[test]
450    fn builder_with_sensor() {
451        let inv = Inventory::builder()
452            .source("Test")
453            .network("XX", |net| {
454                net.station("TEST", |sta| {
455                    sta.latitude(0.0)
456                        .longitude(0.0)
457                        .elevation(0.0)
458                        .site_name("Test")
459                        .channel("SHZ", "00", |ch| {
460                            ch.azimuth(0.0)
461                                .dip(-90.0)
462                                .sample_rate(100.0)
463                                .sensor(Equipment {
464                                    equipment_type: Some("Geophone".into()),
465                                    model: Some("GS-11D".into()),
466                                    manufacturer: Some("Geospace".into()),
467                                    ..Default::default()
468                                })
469                        })
470                })
471            })
472            .build();
473
474        let sensor = inv.networks[0].stations[0].channels[0]
475            .sensor
476            .as_ref()
477            .unwrap();
478        assert_eq!(sensor.model.as_deref(), Some("GS-11D"));
479    }
480}