Skip to main content

bgpkit_broker/
lib.rs

1/*!
2# Overview
3
4[bgpkit-broker][crate] is a package that allows accessing the BGPKIT Broker API and search for BGP archive
5files with different search parameters available.
6
7# Examples
8
9## Basic Usage with Iterator
10
11The recommended usage to collect [BrokerItem]s is to use the built-in iterator. The
12[BrokerItemIterator] handles making API queries so that it can continuously stream new items until
13it reaches the end of items. This is useful for simply getting **all** matching items without need
14to worry about pagination.
15
16```no_run
17use bgpkit_broker::{BgpkitBroker, BrokerItem};
18
19let broker = BgpkitBroker::new()
20    .ts_start("2022-01-01")
21    .ts_end("2022-01-02")
22    .collector_id("route-views2");
23
24// Iterate by reference (reusable broker)
25for item in &broker {
26    println!("BGP file: {} from {} ({})",
27             item.url, item.collector_id, item.data_type);
28}
29
30// Or collect into vector
31let items: Vec<BrokerItem> = broker.into_iter().collect();
32println!("Found {} BGP archive files", items.len());
33```
34
35## Practical BGP Data Analysis with Shortcuts
36
37The SDK provides convenient shortcuts for common BGP data analysis patterns:
38
39### Daily RIB Analysis Across Diverse Collectors
40
41```no_run
42use bgpkit_broker::BgpkitBroker;
43
44// Find the most diverse collectors for comprehensive analysis
45let broker = BgpkitBroker::new()
46    .ts_start("2024-01-01")
47    .ts_end("2024-01-31");
48
49let diverse_collectors = broker.most_diverse_collectors(5, None).unwrap();
50println!("Selected {} diverse collectors: {:?}",
51         diverse_collectors.len(), diverse_collectors);
52
53// Get daily RIB snapshots from these collectors
54let daily_ribs = broker
55    .clone()
56    .collector_id(&diverse_collectors.join(","))
57    .daily_ribs().unwrap();
58
59println!("Found {} daily RIB snapshots for analysis", daily_ribs.len());
60for rib in daily_ribs.iter().take(3) {
61    println!("Daily snapshot: {} from {} at {}",
62             rib.collector_id,
63             rib.ts_start.format("%Y-%m-%d"),
64             rib.url);
65}
66```
67
68### Recent BGP Updates Monitoring
69
70```no_run
71use bgpkit_broker::BgpkitBroker;
72
73// Monitor recent BGP updates from multiple collectors
74let recent_updates = BgpkitBroker::new()
75    .collector_id("route-views2,rrc00,route-views6")
76    .recent_updates(6).unwrap(); // last 6 hours
77
78println!("Found {} recent BGP update files", recent_updates.len());
79for update in recent_updates.iter().take(5) {
80    println!("Update: {} from {} at {}",
81             update.collector_id,
82             update.ts_start.format("%Y-%m-%d %H:%M:%S"),
83             update.url);
84}
85```
86
87### Project-specific Analysis
88
89```no_run
90use bgpkit_broker::BgpkitBroker;
91
92// Compare RouteViews vs RIPE RIS daily snapshots
93let routeviews_ribs = BgpkitBroker::new()
94    .ts_start("2024-01-01")
95    .ts_end("2024-01-07")
96    .project("routeviews")
97    .daily_ribs().unwrap();
98
99let ripe_ribs = BgpkitBroker::new()
100    .ts_start("2024-01-01")
101    .ts_end("2024-01-07")
102    .project("riperis")
103    .daily_ribs().unwrap();
104
105println!("RouteViews daily RIBs: {}", routeviews_ribs.len());
106println!("RIPE RIS daily RIBs: {}", ripe_ribs.len());
107```
108
109### Advanced Collector Selection
110
111```no_run
112use bgpkit_broker::BgpkitBroker;
113
114let broker = BgpkitBroker::new();
115
116// Get diverse RouteViews collectors for focused analysis
117let rv_collectors = broker.most_diverse_collectors(3, Some("routeviews")).unwrap();
118println!("Diverse RouteViews collectors: {:?}", rv_collectors);
119
120// Use them to get comprehensive recent updates
121let comprehensive_updates = broker
122    .clone()
123    .collector_id(&rv_collectors.join(","))
124    .recent_updates(12).unwrap(); // last 12 hours
125
126println!("Got {} updates from {} collectors",
127         comprehensive_updates.len(), rv_collectors.len());
128```
129
130### Routing Table Snapshot Reconstruction
131
132```no_run
133use bgpkit_broker::BgpkitBroker;
134
135// Get the MRT files needed to construct a routing table snapshot
136let broker = BgpkitBroker::new();
137let snapshots = broker.get_snapshot_files(
138    &["route-views2", "rrc00"],
139    "2024-01-01T12:00:00Z"
140).unwrap();
141
142for snapshot in snapshots {
143    println!("Collector: {}", snapshot.collector_id);
144    println!("RIB dump: {}", snapshot.rib_url);
145    println!("Updates to apply: {}", snapshot.updates_urls.len());
146
147    // Use with bgpkit-parser to reconstruct routing table:
148    // 1. Parse RIB dump for initial state
149    // 2. Apply updates in order to reach target timestamp
150}
151```
152
153## Manual Page Queries
154
155For fine-grained control over pagination or custom iteration patterns:
156
157```rust,no_run
158use bgpkit_broker::BgpkitBroker;
159
160let mut broker = BgpkitBroker::new()
161    .ts_start("2022-01-01")
162    .ts_end("2022-01-02")
163    .page(1)
164    .page_size(50);
165
166// Query specific page
167let page1_items = broker.query_single_page().unwrap();
168println!("Page 1: {} items", page1_items.len());
169
170// Move to next page
171broker.turn_page(2);
172let page2_items = broker.query_single_page().unwrap();
173println!("Page 2: {} items", page2_items.len());
174```
175
176## Getting Latest Files and Peer Information
177
178Access the most recent data and peer information:
179
180```rust,no_run
181use bgpkit_broker::BgpkitBroker;
182
183// Get latest files from all collectors
184let broker = BgpkitBroker::new();
185let latest_files = broker.latest().unwrap();
186println!("Latest files from {} collectors", latest_files.len());
187
188// Get full-feed peers from specific collector
189let peers = BgpkitBroker::new()
190    .collector_id("route-views2")
191    .peers_only_full_feed(true)
192    .get_peers().unwrap();
193
194println!("Found {} full-feed peers", peers.len());
195for peer in peers.iter().take(3) {
196    println!("Peer: AS{} ({}) - v4: {}, v6: {}",
197             peer.asn, peer.ip, peer.num_v4_pfxs, peer.num_v6_pfxs);
198}
199```
200*/
201
202#![doc(
203    html_logo_url = "https://raw.githubusercontent.com/bgpkit/assets/main/logos/icon-transparent.png",
204    html_favicon_url = "https://raw.githubusercontent.com/bgpkit/assets/main/logos/favicon.ico"
205)]
206#![allow(unknown_lints)]
207
208mod collector;
209#[cfg(feature = "cli")]
210pub mod config;
211#[cfg(feature = "cli")]
212mod crawler;
213#[cfg(feature = "backend")]
214pub mod db;
215mod error;
216mod item;
217mod peer;
218mod query;
219mod shortcuts;
220#[cfg(feature = "sse")]
221mod sse;
222
223use crate::collector::DEFAULT_COLLECTORS_CONFIG;
224use crate::peer::BrokerPeersResult;
225use crate::query::{BrokerQueryResult, CollectorLatestResult};
226use chrono::{DateTime, NaiveDate, TimeZone, Utc};
227pub use collector::{load_collectors, Collector};
228
229#[cfg(feature = "cli")]
230pub use config::{BrokerConfig, DatabaseTarget};
231#[cfg(feature = "cli")]
232pub use crawler::crawl_collector;
233#[cfg(feature = "cli")]
234pub use db::DatabaseBackend;
235#[cfg(feature = "backend")]
236pub use db::{LocalBrokerDb, PostgresDb, UpdatesMeta, DEFAULT_PAGE_SIZE};
237pub use error::BrokerError;
238pub use item::BrokerItem;
239pub use peer::BrokerPeer;
240pub use query::{QueryParams, SortOrder};
241pub use shortcuts::SnapshotFiles;
242#[cfg(feature = "sse")]
243pub use sse::{BrokerItemSubscription, SseSubscriptionOptions};
244use std::collections::{HashMap, HashSet};
245use std::fmt::Display;
246use std::net::IpAddr;
247use std::path::PathBuf;
248
249const SDK_USER_AGENT: &str = concat!("bgpkit-broker/", env!("CARGO_PKG_VERSION"));
250
251/// BgpkitBroker struct maintains the broker's URL and handles making API queries.
252///
253/// See [module doc][crate#examples] for usage examples.
254#[derive(Clone)]
255pub struct BgpkitBroker {
256    pub broker_url: String,
257    pub query_params: QueryParams,
258    client: reqwest::blocking::Client,
259    collector_project_map: HashMap<String, String>,
260    accept_invalid_certs: bool,
261    cache_dir: Option<PathBuf>,
262}
263
264impl Default for BgpkitBroker {
265    fn default() -> Self {
266        dotenvy::dotenv().ok();
267        let url = match std::env::var("BGPKIT_BROKER_URL") {
268            Ok(url) => url.trim_end_matches('/').to_string(),
269            Err(_) => "https://api.bgpkit.com/v3/broker".to_string(),
270        };
271
272        let collector_project_map = DEFAULT_COLLECTORS_CONFIG.clone().to_project_map();
273
274        let accept_invalid_certs = read_accept_invalid_certs_from_env();
275        let client = build_blocking_client(accept_invalid_certs);
276
277        Self {
278            broker_url: url,
279            query_params: Default::default(),
280            client,
281            collector_project_map,
282            accept_invalid_certs,
283            cache_dir: None,
284        }
285    }
286}
287
288fn read_accept_invalid_certs_from_env() -> bool {
289    match std::env::var("ONEIO_ACCEPT_INVALID_CERTS") {
290        Ok(t) => {
291            let l = t.to_lowercase();
292            l.starts_with("true") || l.starts_with("y")
293        }
294        Err(_) => false,
295    }
296}
297
298fn build_blocking_client(accept_invalid_certs: bool) -> reqwest::blocking::Client {
299    match reqwest::blocking::ClientBuilder::new()
300        .danger_accept_invalid_certs(accept_invalid_certs)
301        .user_agent(SDK_USER_AGENT)
302        .build()
303    {
304        Ok(c) => c,
305        Err(e) => {
306            panic!("Failed to build HTTP client for broker requests: {}", e);
307        }
308    }
309}
310
311#[cfg(feature = "sse")]
312pub(crate) fn build_async_client(
313    accept_invalid_certs: bool,
314) -> Result<reqwest::Client, BrokerError> {
315    reqwest::ClientBuilder::new()
316        .danger_accept_invalid_certs(accept_invalid_certs)
317        .user_agent(SDK_USER_AGENT)
318        .build()
319        .map_err(BrokerError::NetworkError)
320}
321
322impl BgpkitBroker {
323    /// Construct a new BgpkitBroker object.
324    ///
325    /// The URL and query parameters can be adjusted with other functions.
326    ///
327    /// Users can opt in to accept invalid SSL certificates by setting the environment variable
328    /// `ONEIO_ACCEPT_INVALID_CERTS` to `true`.
329    ///
330    /// # Examples
331    /// ```
332    /// use bgpkit_broker::BgpkitBroker;
333    /// let broker = BgpkitBroker::new();
334    /// ```
335    pub fn new() -> Self {
336        Self::default()
337    }
338
339    /// Configure broker URL.
340    ///
341    /// You can change the default broker URL to point to your own broker instance.
342    /// You can also change the URL by setting the environment variable `BGPKIT_BROKER_URL`.
343    ///
344    /// # Examples
345    /// ```
346    /// let broker = bgpkit_broker::BgpkitBroker::new()
347    ///     .broker_url("api.broker.example.com/v3");
348    /// ```
349    pub fn broker_url<S: Display>(self, url: S) -> Self {
350        let broker_url = url.to_string().trim_end_matches('/').to_string();
351        Self {
352            broker_url,
353            query_params: self.query_params,
354            client: self.client,
355            collector_project_map: self.collector_project_map,
356            accept_invalid_certs: self.accept_invalid_certs,
357            cache_dir: self.cache_dir,
358        }
359    }
360
361    /// DANGER: Accept invalid SSL certificates.
362    pub fn accept_invalid_certs(self) -> Self {
363        Self {
364            broker_url: self.broker_url,
365            query_params: self.query_params,
366            client: build_blocking_client(true),
367            collector_project_map: self.collector_project_map,
368            accept_invalid_certs: true,
369            cache_dir: self.cache_dir,
370        }
371    }
372
373    /// Disable SSL certificate check.
374    #[deprecated(since = "0.7.1", note = "Please use `accept_invalid_certs` instead.")]
375    pub fn disable_ssl_check(self) -> Self {
376        Self::accept_invalid_certs(self)
377    }
378
379    /// Set the cache directory for storing query results.
380    ///
381    /// When a cache directory is specified, query results will be cached to disk
382    /// and loaded from cache on subsequent queries with the same parameters.
383    /// This is useful for development and offline usage.
384    ///
385    /// The directory will be created if it doesn't exist. Panics if unable to create.
386    ///
387    /// # Examples
388    ///
389    /// ```no_run
390    /// let broker = bgpkit_broker::BgpkitBroker::new()
391    ///     .cache_dir("/tmp/bgpkit-cache");
392    /// ```
393    pub fn cache_dir<P: Into<PathBuf>>(mut self, path: P) -> Self {
394        let path = path.into();
395        if !path.exists() {
396            std::fs::create_dir_all(&path).expect("Failed to create cache directory");
397        }
398        self.cache_dir = Some(path);
399        self
400    }
401
402    /// Generate cache key from current query parameters.
403    fn cache_key(&self) -> String {
404        use sha2::{Digest, Sha256};
405
406        let params_str = format!(
407            "{}:{}:{}:{}:{}:{}:{}:{}",
408            self.broker_url,
409            self.query_params.ts_start.as_deref().unwrap_or(""),
410            self.query_params.ts_end.as_deref().unwrap_or(""),
411            self.query_params.collector_id.as_deref().unwrap_or(""),
412            self.query_params.project.as_deref().unwrap_or(""),
413            self.query_params.data_type.as_deref().unwrap_or(""),
414            self.query_params.page,
415            self.query_params.page_size
416        );
417
418        let mut hasher = Sha256::new();
419        hasher.update(params_str.as_bytes());
420        hasher
421            .finalize()
422            .iter()
423            .map(|b| format!("{:02x}", b))
424            .collect::<String>()
425    }
426
427    /// Try to load cached results for current query parameters.
428    fn load_cache(&self) -> Option<Vec<BrokerItem>> {
429        let cache_dir = self.cache_dir.as_ref()?;
430        let cache_file = cache_dir.join(self.cache_key()).with_extension("json");
431
432        if !cache_file.exists() {
433            return None;
434        }
435
436        match std::fs::read_to_string(&cache_file) {
437            Ok(contents) => match serde_json::from_str::<Vec<BrokerItem>>(&contents) {
438                Ok(items) => {
439                    log::info!("Loaded {} items from cache", items.len());
440                    Some(items)
441                }
442                Err(e) => {
443                    log::warn!("Failed to deserialize cache file: {}", e);
444                    None
445                }
446            },
447            Err(e) => {
448                log::warn!("Failed to read cache file: {}", e);
449                None
450            }
451        }
452    }
453
454    /// Save results to cache for current query parameters.
455    fn save_cache(&self, items: &[BrokerItem]) {
456        let Some(cache_dir) = self.cache_dir.as_ref() else {
457            return;
458        };
459
460        let cache_file = cache_dir.join(self.cache_key()).with_extension("json");
461
462        match serde_json::to_string(items) {
463            Ok(json) => {
464                if let Err(e) = std::fs::write(&cache_file, json) {
465                    log::warn!("Failed to write cache file: {}", e);
466                } else {
467                    log::info!("Saved {} items to cache", items.len());
468                }
469            }
470            Err(e) => {
471                log::warn!("Failed to serialize items for cache: {}", e);
472            }
473        }
474    }
475
476    /// Parse and validate timestamp string with support for multiple formats.
477    ///
478    /// Supported formats:
479    /// - Unix timestamp: "1640995200"
480    /// - RFC3339/ISO8601: "2022-01-01T00:00:00Z", "2022-01-01T12:30:45Z"
481    /// - RFC3339 without Z: "2022-01-01T00:00:00", "2022-01-01T12:30:45"
482    /// - Date with time: "2022-01-01 00:00:00", "2022-01-01 12:30:45"
483    /// - Pure date (start of day): "2022-01-01", "2022/01/01"
484    /// - Pure date with dots: "2022.01.01"
485    /// - Compact date: "20220101"
486    ///
487    /// For pure date formats, the time component defaults to 00:00:00 (start of day).
488    /// Returns a `DateTime<Utc>` for consistent handling and formatting.
489    fn parse_timestamp(timestamp: &str) -> Result<DateTime<Utc>, BrokerError> {
490        let ts_str = timestamp.trim();
491
492        // Try parsing as RFC3339 with timezone (including +00:00, -05:00, Z, etc.)
493        if let Ok(dt_with_tz) = DateTime::parse_from_rfc3339(ts_str) {
494            return Ok(dt_with_tz.with_timezone(&Utc));
495        }
496
497        // Try parsing as RFC3339/ISO8601 with Z
498        if let Ok(naive_dt) = chrono::NaiveDateTime::parse_from_str(ts_str, "%Y-%m-%dT%H:%M:%SZ") {
499            return Ok(Utc.from_utc_datetime(&naive_dt));
500        }
501
502        // Try parsing as RFC3339 without Z (assume UTC)
503        if let Ok(naive_dt) = chrono::NaiveDateTime::parse_from_str(ts_str, "%Y-%m-%dT%H:%M:%S") {
504            return Ok(Utc.from_utc_datetime(&naive_dt));
505        }
506
507        // Try parsing as "YYYY-MM-DD HH:MM:SS" (assume UTC)
508        if let Ok(naive_dt) = chrono::NaiveDateTime::parse_from_str(ts_str, "%Y-%m-%d %H:%M:%S") {
509            return Ok(Utc.from_utc_datetime(&naive_dt));
510        }
511
512        // Try parsing pure date formats and convert to start of day
513        let date_formats = [
514            "%Y-%m-%d", // 2022-01-01
515            "%Y/%m/%d", // 2022/01/01
516            "%Y.%m.%d", // 2022.01.01
517            "%Y%m%d",   // 20220101 - must be exactly 8 digits
518        ];
519
520        for format in &date_formats {
521            if let Ok(date) = NaiveDate::parse_from_str(ts_str, format) {
522                // Additional validation for compact format to ensure it's actually a date
523                if format == &"%Y%m%d" && ts_str.len() != 8 {
524                    continue;
525                }
526                // Convert to start of day in UTC
527                if let Some(naive_datetime) = date.and_hms_opt(0, 0, 0) {
528                    return Ok(Utc.from_utc_datetime(&naive_datetime));
529                }
530            }
531        }
532
533        // Finally, try parsing as Unix timestamp (only if it's reasonable length and all digits)
534        if ts_str.len() >= 9 && ts_str.len() <= 13 && ts_str.chars().all(|c| c.is_ascii_digit()) {
535            if let Ok(timestamp) = ts_str.parse::<i64>() {
536                if let Some(dt) = Utc.timestamp_opt(timestamp, 0).single() {
537                    return Ok(dt);
538                }
539            }
540        }
541
542        Err(BrokerError::ConfigurationError(format!(
543            "Invalid timestamp format '{ts_str}'. Supported formats:\n\
544                - Unix timestamp: '1640995200'\n\
545                - RFC3339 with timezone: '2022-01-01T00:00:00+00:00', '2022-01-01T00:00:00Z', '2022-01-01T05:00:00-05:00'\n\
546                - RFC3339 without timezone: '2022-01-01T00:00:00' (assumes UTC)\n\
547                - Date with time: '2022-01-01 00:00:00'\n\
548                - Pure date: '2022-01-01', '2022/01/01', '2022.01.01', '20220101'"
549        )))
550    }
551
552    /// Validate all configuration parameters before making API calls.
553    ///
554    /// This performs the same validation that was previously done at configuration time,
555    /// but now happens just before queries are executed. Returns normalized query parameters.
556    fn validate_configuration(&self) -> Result<QueryParams, BrokerError> {
557        // Validate timestamps and normalize them
558        let mut normalized_params = self.query_params.clone();
559
560        // Apply default 30-day time window if no timestamps specified
561        // This prevents slow full-table scans and avoids returning phantom 1970 entries
562        if normalized_params.ts_start.is_none() && normalized_params.ts_end.is_none() {
563            let now = chrono::Utc::now();
564            let thirty_days_ago = now - chrono::Duration::days(30);
565            normalized_params.ts_start =
566                Some(thirty_days_ago.format("%Y-%m-%dT%H:%M:%SZ").to_string());
567            normalized_params.ts_end = Some(now.format("%Y-%m-%dT%H:%M:%SZ").to_string());
568        }
569
570        if let Some(ts) = &self.query_params.ts_start {
571            let parsed_datetime = Self::parse_timestamp(ts)?;
572            normalized_params.ts_start =
573                Some(parsed_datetime.format("%Y-%m-%dT%H:%M:%SZ").to_string());
574        }
575
576        if let Some(ts) = &self.query_params.ts_end {
577            let parsed_datetime = Self::parse_timestamp(ts)?;
578            normalized_params.ts_end =
579                Some(parsed_datetime.format("%Y-%m-%dT%H:%M:%SZ").to_string());
580        }
581
582        // Permissive collector validation: normalize only, no network I/O
583        if let Some(collector_str) = &self.query_params.collector_id {
584            let collectors: Vec<String> = collector_str
585                .split(',')
586                .map(|s| s.trim())
587                .filter(|s| !s.is_empty())
588                .map(|s| s.to_string())
589                .collect();
590
591            if collectors.is_empty() {
592                return Err(BrokerError::ConfigurationError(
593                    "Collector ID cannot be empty".to_string(),
594                ));
595            }
596
597            // Deduplicate while preserving order
598            let mut seen = HashSet::new();
599            let mut deduped = Vec::with_capacity(collectors.len());
600            for c in collectors {
601                if seen.insert(c.clone()) {
602                    deduped.push(c);
603                }
604            }
605
606            normalized_params.collector_id = Some(deduped.join(","));
607        }
608
609        // Validate project
610        if let Some(project_str) = &self.query_params.project {
611            let project_lower = project_str.to_lowercase();
612            match project_lower.as_str() {
613                "rrc" | "riperis" | "ripe_ris" | "routeviews" | "route_views" | "rv" => {
614                    // Valid project
615                }
616                _ => {
617                    return Err(BrokerError::ConfigurationError(format!(
618                        "Invalid project '{project_str}'. Valid projects are: 'riperis' (aliases: 'rrc', 'ripe_ris') or 'routeviews' (aliases: 'route_views', 'rv')"
619                    )));
620                }
621            }
622        }
623
624        // Validate data type
625        if let Some(data_type_str) = &self.query_params.data_type {
626            let data_type_lower = data_type_str.to_lowercase();
627            match data_type_lower.as_str() {
628                "rib" | "ribs" | "r" | "update" | "updates" => {
629                    // Valid data type
630                }
631                _ => {
632                    return Err(BrokerError::ConfigurationError(format!(
633                        "Invalid data type '{data_type_str}'. Valid data types are: 'rib' (aliases: 'ribs', 'r') or 'updates' (alias: 'update')"
634                    )));
635                }
636            }
637        }
638
639        // Validate page number
640        if self.query_params.page < 1 {
641            return Err(BrokerError::ConfigurationError(format!(
642                "Invalid page number {}. Page number must be >= 1",
643                self.query_params.page
644            )));
645        }
646
647        // Validate page size
648        if !(1..=100000).contains(&self.query_params.page_size) {
649            return Err(BrokerError::ConfigurationError(format!(
650                "Invalid page size {}. Page size must be between 1 and 100000",
651                self.query_params.page_size
652            )));
653        }
654
655        Ok(normalized_params)
656    }
657
658    /// Add a filter of starting timestamp.
659    ///
660    /// Supports multiple timestamp formats including Unix timestamps, RFC3339 dates, and pure dates.
661    /// Validation occurs at query time.
662    ///
663    /// # Examples
664    ///
665    /// Specify a Unix timestamp:
666    /// ```
667    /// let broker = bgpkit_broker::BgpkitBroker::new()
668    ///     .ts_start("1640995200");
669    /// ```
670    ///
671    /// Specify a RFC3339-formatted time string:
672    /// ```
673    /// let broker = bgpkit_broker::BgpkitBroker::new()
674    ///     .ts_start("2022-01-01T00:00:00Z");
675    /// ```
676    ///
677    /// Specify a pure date (defaults to start of day):
678    /// ```
679    /// let broker = bgpkit_broker::BgpkitBroker::new()
680    ///     .ts_start("2022-01-01");
681    /// ```
682    ///
683    /// Other supported formats:
684    /// ```
685    /// let broker = bgpkit_broker::BgpkitBroker::new()
686    ///     .ts_start("2022/01/01")  // slash format
687    ///     .ts_start("2022.01.01")  // dot format
688    ///     .ts_start("20220101");   // compact format
689    /// ```
690    pub fn ts_start<S: Display>(self, ts_start: S) -> Self {
691        let mut query_params = self.query_params;
692        query_params.ts_start = Some(ts_start.to_string());
693        Self {
694            broker_url: self.broker_url,
695            query_params,
696            client: self.client,
697            collector_project_map: self.collector_project_map,
698            accept_invalid_certs: self.accept_invalid_certs,
699            cache_dir: self.cache_dir,
700        }
701    }
702
703    /// Add a filter of ending timestamp.
704    ///
705    /// Supports the same multiple timestamp formats as `ts_start`.
706    /// Validation occurs at query time.
707    ///
708    /// # Examples
709    ///
710    /// Specify a Unix timestamp:
711    /// ```
712    /// let broker = bgpkit_broker::BgpkitBroker::new()
713    ///     .ts_end("1640995200");
714    /// ```
715    ///
716    /// Specify a RFC3339-formatted time string:
717    /// ```
718    /// let broker = bgpkit_broker::BgpkitBroker::new()
719    ///     .ts_end("2022-01-01T00:00:00Z");
720    /// ```
721    ///
722    /// Specify a pure date (defaults to start of day):
723    /// ```
724    /// let broker = bgpkit_broker::BgpkitBroker::new()
725    ///     .ts_end("2022-01-01");
726    /// ```
727    pub fn ts_end<S: Display>(self, ts_end: S) -> Self {
728        let mut query_params = self.query_params;
729        query_params.ts_end = Some(ts_end.to_string());
730        Self {
731            broker_url: self.broker_url,
732            client: self.client,
733            query_params,
734            collector_project_map: self.collector_project_map,
735            accept_invalid_certs: self.accept_invalid_certs,
736            cache_dir: self.cache_dir,
737        }
738    }
739
740    /// Add a filter of collector ID (e.g. `rrc00` or `route-views2`).
741    ///
742    /// See the full list of collectors [here](https://github.com/bgpkit/bgpkit-broker-backend/blob/main/deployment/full-config.json).
743    /// Validation occurs at query time.
744    ///
745    /// # Examples
746    ///
747    /// filter by single collector
748    /// ```
749    /// let broker = bgpkit_broker::BgpkitBroker::new()
750    ///     .collector_id("rrc00");
751    /// ```
752    ///
753    /// filter by multiple collector
754    /// ```
755    /// let broker = bgpkit_broker::BgpkitBroker::new()
756    ///     .collector_id("route-views2,route-views6");
757    /// ```
758    pub fn collector_id<S: Display>(self, collector_id: S) -> Self {
759        let mut query_params = self.query_params;
760        query_params.collector_id = Some(collector_id.to_string());
761        Self {
762            client: self.client,
763            broker_url: self.broker_url,
764            query_params,
765            collector_project_map: self.collector_project_map,
766            accept_invalid_certs: self.accept_invalid_certs,
767            cache_dir: self.cache_dir,
768        }
769    }
770
771    /// Add a filter of project name with validation, i.e. `riperis` or `routeviews`.
772    ///
773    /// # Examples
774    ///
775    /// ```
776    /// let broker = bgpkit_broker::BgpkitBroker::new()
777    ///     .project("riperis");
778    /// ```
779    ///
780    /// ```
781    /// let broker = bgpkit_broker::BgpkitBroker::new()
782    ///     .project("routeviews");
783    /// ```
784    pub fn project<S: Display>(self, project: S) -> Self {
785        let mut query_params = self.query_params;
786        query_params.project = Some(project.to_string());
787        Self {
788            client: self.client,
789            broker_url: self.broker_url,
790            query_params,
791            collector_project_map: self.collector_project_map,
792            accept_invalid_certs: self.accept_invalid_certs,
793            cache_dir: self.cache_dir,
794        }
795    }
796
797    /// Add filter of data type, i.e. `rib` or `updates`.
798    ///
799    /// Validation occurs at query time.
800    ///
801    /// # Examples
802    ///
803    /// ```
804    /// let broker = bgpkit_broker::BgpkitBroker::new()
805    ///     .data_type("rib");
806    /// ```
807    ///
808    /// ```
809    /// let broker = bgpkit_broker::BgpkitBroker::new()
810    ///     .data_type("updates");
811    /// ```
812    pub fn data_type<S: Display>(self, data_type: S) -> Self {
813        let mut query_params = self.query_params;
814        query_params.data_type = Some(data_type.to_string());
815        Self {
816            broker_url: self.broker_url,
817            client: self.client,
818            query_params,
819            collector_project_map: self.collector_project_map,
820            accept_invalid_certs: self.accept_invalid_certs,
821            cache_dir: self.cache_dir,
822        }
823    }
824
825    /// Change the current page number, starting from 1.
826    ///
827    /// Validation occurs at query time.
828    ///
829    /// # Examples
830    ///
831    /// Start iterating with page 2.
832    /// ```
833    /// let broker = bgpkit_broker::BgpkitBroker::new()
834    ///     .page(2);
835    /// ```
836    pub fn page(self, page: i64) -> Self {
837        let mut query_params = self.query_params;
838        query_params.page = page;
839        Self {
840            broker_url: self.broker_url,
841            client: self.client,
842            query_params,
843            collector_project_map: self.collector_project_map,
844            accept_invalid_certs: self.accept_invalid_certs,
845            cache_dir: self.cache_dir,
846        }
847    }
848
849    /// Change current page size, default 100.
850    ///
851    /// Validation occurs at query time.
852    ///
853    /// # Examples
854    ///
855    /// Set page size to 20.
856    /// ```
857    /// let broker = bgpkit_broker::BgpkitBroker::new()
858    ///     .page_size(10);
859    /// ```
860    pub fn page_size(self, page_size: i64) -> Self {
861        let mut query_params = self.query_params;
862        query_params.page_size = page_size;
863        Self {
864            broker_url: self.broker_url,
865            client: self.client,
866            query_params,
867            collector_project_map: self.collector_project_map,
868            accept_invalid_certs: self.accept_invalid_certs,
869            cache_dir: self.cache_dir,
870        }
871    }
872
873    /// Add a filter of peer IP address when listing peers.
874    ///
875    /// # Examples
876    ///
877    /// ```
878    /// let broker = bgpkit_broker::BgpkitBroker::new()
879    ///    .peers_ip("192.168.1.1".parse().unwrap());
880    /// ```
881    pub fn peers_ip(self, peer_ip: IpAddr) -> Self {
882        let mut query_params = self.query_params;
883        query_params.peers_ip = Some(peer_ip);
884        Self {
885            broker_url: self.broker_url,
886            client: self.client,
887            query_params,
888            collector_project_map: self.collector_project_map,
889            accept_invalid_certs: self.accept_invalid_certs,
890            cache_dir: self.cache_dir,
891        }
892    }
893
894    /// Add a filter of peer ASN when listing peers.
895    ///
896    /// # Examples
897    ///
898    /// ```
899    /// let broker = bgpkit_broker::BgpkitBroker::new()
900    ///    .peers_asn(64496);
901    /// ```
902    pub fn peers_asn(self, peer_asn: u32) -> Self {
903        let mut query_params = self.query_params;
904        query_params.peers_asn = Some(peer_asn);
905        Self {
906            broker_url: self.broker_url,
907            client: self.client,
908            query_params,
909            collector_project_map: self.collector_project_map,
910            accept_invalid_certs: self.accept_invalid_certs,
911            cache_dir: self.cache_dir,
912        }
913    }
914
915    /// Add a filter of peer full feed status when listing peers.
916    ///
917    /// # Examples
918    ///
919    /// ```
920    /// let broker = bgpkit_broker::BgpkitBroker::new()
921    ///   .peers_only_full_feed(true);
922    /// ```
923    pub fn peers_only_full_feed(self, peer_full_feed: bool) -> Self {
924        let mut query_params = self.query_params;
925        query_params.peers_only_full_feed = peer_full_feed;
926        Self {
927            broker_url: self.broker_url,
928            client: self.client,
929            query_params,
930            collector_project_map: self.collector_project_map,
931            accept_invalid_certs: self.accept_invalid_certs,
932            cache_dir: self.cache_dir,
933        }
934    }
935
936    /// Turn to specified page, page starting from 1.
937    ///
938    /// This works with [Self::query_single_page] function to manually paginate.
939    ///
940    /// # Examples
941    ///
942    /// Manually get the first two pages of items.
943    /// ```no_run
944    /// let mut broker = bgpkit_broker::BgpkitBroker::new();
945    /// let mut items = vec![];
946    /// items.extend(broker.query_single_page().unwrap());
947    /// broker.turn_page(2);
948    /// items.extend(broker.query_single_page().unwrap());
949    /// ```
950    pub fn turn_page(&mut self, page: i64) {
951        self.query_params.page = page;
952    }
953
954    /// Send API for a single page of items.
955    ///
956    /// # Examples
957    ///
958    /// Manually get the first page of items.
959    /// ```no_run
960    /// let broker = bgpkit_broker::BgpkitBroker::new();
961    /// let items = broker.query_single_page().unwrap();
962    /// ```
963    pub fn query_single_page(&self) -> Result<Vec<BrokerItem>, BrokerError> {
964        // Try to load from cache first
965        if let Some(cached_items) = self.load_cache() {
966            return Ok(cached_items);
967        }
968
969        let validated_params = self.validate_configuration()?;
970        let url = format!("{}/search{}", self.broker_url, validated_params);
971        log::info!("sending broker query to {}", url);
972        match self.run_files_query(url.as_str()) {
973            Ok(res) => {
974                // Save to cache if cache_dir is set
975                self.save_cache(&res.data);
976                Ok(res.data)
977            }
978            Err(e) => Err(e),
979        }
980    }
981
982    /// Query the total count of items matching the current search criteria without fetching the items.
983    ///
984    /// This method is useful when you need to know how many items match your search criteria
985    /// without downloading all the items. It performs the same validation as a regular query
986    /// but only returns the count.
987    ///
988    /// # Returns
989    /// - `Ok(i64)`: The total number of matching items
990    /// - `Err(BrokerError)`: If the query fails or the count is missing from the response
991    ///
992    /// # Examples
993    ///
994    /// ```no_run
995    /// use bgpkit_broker::BgpkitBroker;
996    ///
997    /// let broker = BgpkitBroker::new()
998    ///     .ts_start("2024-01-01")
999    ///     .ts_end("2024-01-02")
1000    ///     .collector_id("route-views2");
1001    ///
1002    /// let count = broker.query_total_count().unwrap();
1003    /// println!("Found {} matching items", count);
1004    /// ```
1005    pub fn query_total_count(&self) -> Result<i64, BrokerError> {
1006        let validated_params = self.validate_configuration()?;
1007        let url = format!("{}/search{}", self.broker_url, validated_params);
1008        match self.run_files_query(url.as_str()) {
1009            Ok(res) => res.total.ok_or(BrokerError::BrokerError(
1010                "count not found in response".to_string(),
1011            )),
1012            Err(e) => Err(e),
1013        }
1014    }
1015
1016    /// Check if the broker instance is healthy.
1017    ///
1018    /// # Examples
1019    ///
1020    /// ```no_run
1021    /// let broker = bgpkit_broker::BgpkitBroker::new();
1022    /// assert!(broker.health_check().is_ok())
1023    /// ```
1024    pub fn health_check(&self) -> Result<(), BrokerError> {
1025        let url = format!("{}/health", self.broker_url.trim_end_matches('/'));
1026        match self.client.get(url.as_str()).send() {
1027            Ok(response) => {
1028                if response.status() == reqwest::StatusCode::OK {
1029                    Ok(())
1030                } else {
1031                    Err(BrokerError::BrokerError(format!(
1032                        "endpoint unhealthy {}",
1033                        self.broker_url
1034                    )))
1035                }
1036            }
1037            Err(_e) => Err(BrokerError::BrokerError(format!(
1038                "endpoint unhealthy {}",
1039                self.broker_url
1040            ))),
1041        }
1042    }
1043
1044    /// Send a query to get **all** data times returned.
1045    ///
1046    /// This usually is what one needs.
1047    ///
1048    /// # Examples
1049    ///
1050    /// Get all RIB files on 2022-01-01 from route-views2.
1051    /// ```no_run
1052    /// let broker = bgpkit_broker::BgpkitBroker::new()
1053    ///     .ts_start("2022-01-01T00:00:00Z")
1054    ///     .ts_end("2022-01-01T23:59:00Z")
1055    ///     .data_type("rib")
1056    ///     .collector_id("route-views2");
1057    /// let items = broker.query().unwrap();
1058    ///
1059    /// // 1 RIB dump very 2 hours, total of 12 files for 1 day
1060    /// assert_eq!(items.len(), 12);
1061    /// ```
1062    pub fn query(&self) -> Result<Vec<BrokerItem>, BrokerError> {
1063        let mut p = self.validate_configuration()?;
1064
1065        let mut items = vec![];
1066        loop {
1067            let url = format!("{}/search{}", self.broker_url, p);
1068
1069            let res_items = self.run_files_query(url.as_str())?.data;
1070
1071            let items_count = res_items.len() as i64;
1072
1073            if items_count == 0 {
1074                // reaches the end
1075                break;
1076            }
1077
1078            items.extend(res_items);
1079            let cur_page = p.page;
1080            p = p.page(cur_page + 1);
1081
1082            if items_count < p.page_size {
1083                // reaches the end
1084                break;
1085            }
1086        }
1087        Ok(items)
1088    }
1089
1090    /// Send a query to get the **latest** data for each collector.
1091    ///
1092    /// The returning result is structured as a vector of [CollectorLatestItem] objects.
1093    ///
1094    /// # Examples
1095    ///
1096    /// ```no_run
1097    /// let broker = bgpkit_broker::BgpkitBroker::new();
1098    /// let latest_items = broker.latest().unwrap();
1099    /// for item in &latest_items {
1100    ///     println!("{}", item);
1101    /// }
1102    /// ```
1103    pub fn latest(&self) -> Result<Vec<BrokerItem>, BrokerError> {
1104        let latest_query_url = format!("{}/latest", self.broker_url);
1105        let mut items = match self.client.get(latest_query_url.as_str()).send() {
1106            Ok(response) => match response.json::<CollectorLatestResult>() {
1107                Ok(result) => result.data,
1108                Err(_) => {
1109                    return Err(BrokerError::BrokerError(
1110                        "Error parsing response".to_string(),
1111                    ));
1112                }
1113            },
1114            Err(e) => {
1115                return Err(BrokerError::BrokerError(format!(
1116                    "Unable to connect to the URL ({latest_query_url}): {e}"
1117                )));
1118            }
1119        };
1120
1121        items.retain(|item| {
1122            let mut matches = true;
1123            if let Some(project) = &self.query_params.project {
1124                match project.to_lowercase().as_str() {
1125                    "rrc" | "riperis" | "ripe_ris" => {
1126                        matches = self
1127                            .collector_project_map
1128                            .get(&item.collector_id)
1129                            .cloned()
1130                            .unwrap_or_default()
1131                            .as_str()
1132                            == "riperis";
1133                    }
1134                    "routeviews" | "route_views" | "rv" => {
1135                        matches = self
1136                            .collector_project_map
1137                            .get(&item.collector_id)
1138                            .cloned()
1139                            .unwrap_or_default()
1140                            .as_str()
1141                            == "routeviews";
1142                    }
1143                    _ => {}
1144                }
1145            }
1146
1147            if let Some(data_type) = &self.query_params.data_type {
1148                match data_type.to_lowercase().as_str() {
1149                    "rib" | "ribs" | "r" if !item.is_rib() => {
1150                        // if not RIB file, not match
1151                        matches = false
1152                    }
1153                    "update" | "updates" if item.is_rib() => {
1154                        // if is RIB file, not match
1155                        matches = false
1156                    }
1157                    _ => {}
1158                }
1159            }
1160
1161            if let Some(collector_id) = &self.query_params.collector_id {
1162                let wanted: HashSet<&str> = collector_id
1163                    .split(',')
1164                    .map(|s| s.trim())
1165                    .filter(|s| !s.is_empty())
1166                    .collect();
1167
1168                if !wanted.contains(item.collector_id.as_str()) {
1169                    return false;
1170                }
1171            }
1172
1173            matches
1174        });
1175
1176        Ok(items)
1177    }
1178
1179    /// Get the most recent information for collector peers.
1180    ///
1181    /// The returning result is structured as a vector of [BrokerPeer] objects.
1182    ///
1183    /// # Examples
1184    ///
1185    /// ## Get all peers
1186    ///
1187    /// ```no_run
1188    /// let broker = bgpkit_broker::BgpkitBroker::new();
1189    /// let peers = broker.get_peers().unwrap();
1190    /// for peer in &peers {
1191    ///     println!("{:?}", peer);
1192    /// }
1193    /// ```
1194    ///
1195    /// ## Get peers from a specific collector
1196    ///
1197    /// ```no_run
1198    /// let broker = bgpkit_broker::BgpkitBroker::new()
1199    ///    .collector_id("route-views2");
1200    /// let peers = broker.get_peers().unwrap();
1201    /// for peer in &peers {
1202    ///    println!("{:?}", peer);
1203    /// }
1204    /// ```
1205    ///
1206    /// ## Get peers from a specific ASN
1207    ///
1208    /// ```no_run
1209    /// let broker = bgpkit_broker::BgpkitBroker::new()
1210    ///   .peers_asn(64496);
1211    /// let peers = broker.get_peers().unwrap();
1212    /// for peer in &peers {
1213    ///    println!("{:?}", peer);
1214    /// }
1215    /// ```
1216    ///
1217    /// ## Get peers from a specific IP address
1218    ///
1219    /// ```no_run
1220    /// let broker = bgpkit_broker::BgpkitBroker::new()
1221    ///   .peers_ip("192.168.1.1".parse().unwrap());
1222    /// let peers = broker.get_peers().unwrap();
1223    /// for peer in &peers {
1224    ///   println!("{:?}", peer);
1225    /// }
1226    /// ```
1227    ///
1228    /// ## Get peers with full feed
1229    ///
1230    /// ```no_run
1231    /// let broker = bgpkit_broker::BgpkitBroker::new()
1232    ///  .peers_only_full_feed(true);
1233    /// let peers = broker.get_peers().unwrap();
1234    /// for peer in &peers {
1235    ///     println!("{:?}", peer);
1236    /// }
1237    /// ```
1238    ///
1239    /// ## Get peers from a specific collector with full feed
1240    ///
1241    /// ```no_run
1242    /// let broker = bgpkit_broker::BgpkitBroker::new()
1243    ///  .collector_id("route-views2")
1244    /// .peers_only_full_feed(true);
1245    /// let peers = broker.get_peers().unwrap();
1246    /// for peer in &peers {
1247    ///    println!("{:?}", peer);
1248    /// }
1249    /// ```
1250    pub fn get_peers(&self) -> Result<Vec<BrokerPeer>, BrokerError> {
1251        let mut url = format!("{}/peers", self.broker_url);
1252        let mut param_strings = vec![];
1253        if let Some(ip) = &self.query_params.peers_ip {
1254            param_strings.push(format!("ip={ip}"));
1255        }
1256        if let Some(asn) = &self.query_params.peers_asn {
1257            param_strings.push(format!("asn={asn}"));
1258        }
1259        if self.query_params.peers_only_full_feed {
1260            param_strings.push("full_feed=true".to_string());
1261        }
1262        if let Some(collector_id) = &self.query_params.collector_id {
1263            param_strings.push(format!("collector={collector_id}"));
1264        }
1265        if !param_strings.is_empty() {
1266            let param_string = param_strings.join("&");
1267            url = format!("{url}?{param_string}");
1268        }
1269
1270        let peers = match self.client.get(url.as_str()).send() {
1271            Ok(response) => match response.json::<BrokerPeersResult>() {
1272                Ok(result) => result.data,
1273                Err(_) => {
1274                    return Err(BrokerError::BrokerError(
1275                        "Error parsing response".to_string(),
1276                    ));
1277                }
1278            },
1279            Err(e) => {
1280                return Err(BrokerError::BrokerError(format!(
1281                    "Unable to connect to the URL ({url}): {e}"
1282                )));
1283            }
1284        };
1285        Ok(peers)
1286    }
1287
1288    fn run_files_query(&self, url: &str) -> Result<BrokerQueryResult, BrokerError> {
1289        log::info!("sending broker query to {}", url);
1290        match self.client.get(url).send() {
1291            Ok(res) => match res.json::<BrokerQueryResult>() {
1292                Ok(res) => {
1293                    if let Some(e) = res.error {
1294                        Err(BrokerError::BrokerError(e))
1295                    } else {
1296                        Ok(res)
1297                    }
1298                }
1299                Err(e) => {
1300                    // json decoding error. most likely the service returns an error message without
1301                    // `data` field.
1302                    Err(BrokerError::BrokerError(e.to_string()))
1303                }
1304            },
1305            Err(e) => Err(BrokerError::from(e)),
1306        }
1307    }
1308}
1309
1310/// Iterator for BGPKIT Broker that iterates through one [BrokerItem] at a time.
1311///
1312/// The [IntoIterator] trait is implemented for both the struct and the reference, so that you can
1313/// either iterate through items by taking the ownership of the broker, or use the reference to broker
1314/// to iterate.
1315///
1316/// ```no_run
1317/// use bgpkit_broker::{BgpkitBroker, BrokerItem};
1318///
1319/// let mut broker = BgpkitBroker::new()
1320///     .ts_start("1634693400")
1321///     .ts_end("1634693400")
1322///     .page_size(10)
1323///     .page(2);
1324///
1325/// // create iterator from reference (so that you can reuse the broker object)
1326/// // same as `&broker.into_intr()`
1327/// for item in &broker {
1328///     println!("{}", item);
1329/// }
1330///
1331/// // create iterator from the broker object (taking ownership)
1332/// let items = broker.into_iter().collect::<Vec<BrokerItem>>();
1333///
1334/// assert_eq!(items.len(), 43);
1335/// ```
1336pub struct BrokerItemIterator {
1337    broker: BgpkitBroker,
1338    cached_items: Vec<BrokerItem>,
1339    first_run: bool,
1340}
1341
1342impl BrokerItemIterator {
1343    pub fn new(broker: BgpkitBroker) -> BrokerItemIterator {
1344        BrokerItemIterator {
1345            broker,
1346            cached_items: vec![],
1347            first_run: true,
1348        }
1349    }
1350}
1351
1352impl Iterator for BrokerItemIterator {
1353    type Item = BrokerItem;
1354
1355    fn next(&mut self) -> Option<Self::Item> {
1356        // if we have cached items, simply pop and return
1357        if let Some(item) = self.cached_items.pop() {
1358            return Some(item);
1359        }
1360
1361        // no more cached items, refill cache by one more broker query
1362        if self.first_run {
1363            // if it's the first time running, do not change page, and switch the flag.
1364            self.first_run = false;
1365        } else {
1366            // if it's not the first time running, add page number by one.
1367            self.broker.query_params.page += 1;
1368        }
1369
1370        // query the current page
1371        let items = match self.broker.query_single_page() {
1372            Ok(i) => i,
1373            Err(_) => return None,
1374        };
1375
1376        if items.is_empty() {
1377            // break out the iteration
1378            return None;
1379        } else {
1380            // fill the cache
1381            self.cached_items = items;
1382            self.cached_items.reverse();
1383        }
1384
1385        #[allow(clippy::unwrap_used)]
1386        Some(self.cached_items.pop().unwrap())
1387    }
1388}
1389
1390impl IntoIterator for BgpkitBroker {
1391    type Item = BrokerItem;
1392    type IntoIter = BrokerItemIterator;
1393
1394    fn into_iter(self) -> Self::IntoIter {
1395        BrokerItemIterator::new(self)
1396    }
1397}
1398
1399impl IntoIterator for &BgpkitBroker {
1400    type Item = BrokerItem;
1401    type IntoIter = BrokerItemIterator;
1402
1403    fn into_iter(self) -> Self::IntoIter {
1404        BrokerItemIterator::new(self.clone())
1405    }
1406}
1407
1408#[cfg(test)]
1409mod tests {
1410    use super::*;
1411
1412    #[test]
1413    fn test_query() {
1414        let broker = BgpkitBroker::new()
1415            .ts_start("1634693400")
1416            .ts_end("1634693400");
1417        let res = broker.query();
1418        assert!(&res.is_ok());
1419        let data = res.unwrap();
1420        assert!(!data.is_empty());
1421    }
1422
1423    #[test]
1424    fn test_network_error() {
1425        let broker = BgpkitBroker::new().broker_url("https://api.broker.example.com/v2");
1426        let res = broker.query();
1427        // when testing a must-fail query, you could use `matches!` macro to do so
1428        assert!(res.is_err());
1429        assert!(matches!(res.err(), Some(BrokerError::NetworkError(_))));
1430    }
1431
1432    #[test]
1433    fn test_broker_error() {
1434        let broker = BgpkitBroker::new().page(-1);
1435        let result = broker.query();
1436        assert!(result.is_err());
1437        assert!(matches!(
1438            result.err(),
1439            Some(BrokerError::ConfigurationError(_))
1440        ));
1441    }
1442
1443    #[test]
1444    fn test_query_all() {
1445        let broker = BgpkitBroker::new()
1446            .ts_start("1634693400")
1447            .ts_end("1634693400")
1448            .page_size(100);
1449        let res = broker.query();
1450        assert!(res.is_ok());
1451        assert!(res.ok().unwrap().len() >= 54);
1452    }
1453
1454    #[test]
1455    fn test_iterator() {
1456        let broker = BgpkitBroker::new()
1457            .ts_start("1634693400")
1458            .ts_end("1634693400");
1459        assert!(broker.into_iter().count() >= 54);
1460    }
1461
1462    #[test]
1463    fn test_filters() {
1464        let broker = BgpkitBroker::new()
1465            .ts_start("1634693400")
1466            .ts_end("1634693400");
1467        let items = broker.query().unwrap();
1468        assert!(items.len() >= 54);
1469
1470        let broker = BgpkitBroker::new()
1471            .ts_start("1634693400")
1472            .ts_end("1634693400")
1473            .collector_id("rrc00");
1474        let items = broker.query().unwrap();
1475        assert_eq!(items.len(), 1);
1476
1477        let broker = BgpkitBroker::new()
1478            .ts_start("1634693400")
1479            .ts_end("1634693400")
1480            .project("riperis");
1481        let items = broker.query().unwrap();
1482        assert_eq!(items.len(), 23);
1483    }
1484
1485    #[test]
1486    fn test_latest() {
1487        let broker = BgpkitBroker::new();
1488        let items = broker.latest().unwrap();
1489        assert!(items.len() >= 125);
1490
1491        let broker = BgpkitBroker::new().project("routeviews".to_string());
1492        let items = broker.latest().unwrap();
1493        assert!(!items.is_empty());
1494        assert!(items
1495            .iter()
1496            .all(|item| !item.collector_id.starts_with("rrc")));
1497
1498        let broker = BgpkitBroker::new().project("riperis".to_string());
1499        let items = broker.latest().unwrap();
1500        assert!(!items.is_empty());
1501        assert!(items
1502            .iter()
1503            .all(|item| item.collector_id.starts_with("rrc")));
1504
1505        let broker = BgpkitBroker::new().data_type("rib".to_string());
1506        let items = broker.latest().unwrap();
1507        assert!(!items.is_empty());
1508        assert!(items.iter().all(|item| item.is_rib()));
1509
1510        let broker = BgpkitBroker::new().data_type("update".to_string());
1511        let items = broker.latest().unwrap();
1512        assert!(!items.is_empty());
1513        assert!(items.iter().all(|item| !item.is_rib()));
1514
1515        let broker = BgpkitBroker::new().collector_id("rrc00".to_string());
1516        let items = broker.latest().unwrap();
1517        assert!(!items.is_empty());
1518        assert!(items
1519            .iter()
1520            .all(|item| item.collector_id.as_str() == "rrc00"));
1521        assert_eq!(items.len(), 2);
1522    }
1523
1524    #[test]
1525    fn test_latest_no_ssl() {
1526        let broker = BgpkitBroker::new().accept_invalid_certs();
1527        let items = broker.latest().unwrap();
1528        assert!(items.len() >= 125);
1529    }
1530
1531    #[test]
1532    fn test_health_check() {
1533        let broker = BgpkitBroker::new();
1534        let res = broker.health_check();
1535        assert!(res.is_ok());
1536    }
1537
1538    #[test]
1539    fn test_peers() {
1540        let broker = BgpkitBroker::new();
1541        let all_peers = broker.get_peers().unwrap();
1542        assert!(!all_peers.is_empty());
1543        let first_peer = all_peers.first().unwrap();
1544        let first_ip = first_peer.ip;
1545        let first_asn = first_peer.asn;
1546
1547        let broker = BgpkitBroker::new().peers_ip(first_ip);
1548        let peers = broker.get_peers().unwrap();
1549        assert!(!peers.is_empty());
1550
1551        let broker = BgpkitBroker::new().peers_asn(first_asn);
1552        let peers = broker.get_peers().unwrap();
1553        assert!(!peers.is_empty());
1554
1555        let broker = BgpkitBroker::new().peers_only_full_feed(true);
1556        let full_feed_peers = broker.get_peers().unwrap();
1557        assert!(!full_feed_peers.is_empty());
1558        assert!(full_feed_peers.len() < all_peers.len());
1559
1560        let broker = BgpkitBroker::new().collector_id("rrc00");
1561        let rrc_peers = broker.get_peers().unwrap();
1562        assert!(!rrc_peers.is_empty());
1563        assert!(rrc_peers.iter().all(|peer| peer.collector == "rrc00"));
1564
1565        let broker = BgpkitBroker::new().collector_id("rrc00,route-views2");
1566        let rrc_rv_peers = broker.get_peers().unwrap();
1567        assert!(!rrc_rv_peers.is_empty());
1568        assert!(rrc_rv_peers
1569            .iter()
1570            .any(|peer| peer.collector == "rrc00" || peer.collector == "route-views2"));
1571
1572        assert!(rrc_rv_peers.len() > rrc_peers.len());
1573    }
1574
1575    #[test]
1576    fn test_timestamp_parsing_unix() {
1577        let broker = BgpkitBroker::new();
1578
1579        // Valid Unix timestamps - configuration succeeds, normalization happens at query time
1580        let result = broker.clone().ts_start("1640995200");
1581        // Raw input is stored during configuration
1582        assert_eq!(result.query_params.ts_start, Some("1640995200".to_string()));
1583
1584        let result = broker.clone().ts_end("1640995200");
1585        assert_eq!(result.query_params.ts_end, Some("1640995200".to_string()));
1586    }
1587
1588    #[test]
1589    fn test_timestamp_parsing_rfc3339() {
1590        let broker = BgpkitBroker::new();
1591
1592        // RFC3339 with Z - raw input stored during configuration
1593        let result = broker.clone().ts_start("2022-01-01T00:00:00Z");
1594        assert_eq!(
1595            result.query_params.ts_start,
1596            Some("2022-01-01T00:00:00Z".to_string())
1597        );
1598
1599        // RFC3339 without Z - raw input stored during configuration
1600        let result = broker.clone().ts_start("2022-01-01T12:30:45");
1601        assert_eq!(
1602            result.query_params.ts_start,
1603            Some("2022-01-01T12:30:45".to_string())
1604        );
1605
1606        // Date with time format - raw input stored during configuration
1607        let result = broker.clone().ts_end("2022-01-01 12:30:45");
1608        assert_eq!(
1609            result.query_params.ts_end,
1610            Some("2022-01-01 12:30:45".to_string())
1611        );
1612    }
1613
1614    #[test]
1615    fn test_timestamp_parsing_pure_dates() {
1616        let broker = BgpkitBroker::new();
1617
1618        // Standard date format - raw input stored during configuration
1619        let result = broker.clone().ts_start("2022-01-01");
1620        assert_eq!(result.query_params.ts_start, Some("2022-01-01".to_string()));
1621
1622        // Slash format
1623        let result = broker.clone().ts_start("2022/01/01");
1624        assert_eq!(result.query_params.ts_start, Some("2022/01/01".to_string()));
1625
1626        // Dot format
1627        let result = broker.clone().ts_end("2022.01.01");
1628        assert_eq!(result.query_params.ts_end, Some("2022.01.01".to_string()));
1629
1630        // Compact format
1631        let result = broker.clone().ts_end("20220101");
1632        assert_eq!(result.query_params.ts_end, Some("20220101".to_string()));
1633    }
1634
1635    #[test]
1636    fn test_timestamp_parsing_whitespace() {
1637        let broker = BgpkitBroker::new();
1638
1639        // Test that raw input with whitespace is stored during configuration
1640        let result = broker.clone().ts_start("  2022-01-01  ");
1641        assert_eq!(
1642            result.query_params.ts_start,
1643            Some("  2022-01-01  ".to_string())
1644        );
1645
1646        let result = broker.clone().ts_end("\t1640995200\n");
1647        assert_eq!(
1648            result.query_params.ts_end,
1649            Some("\t1640995200\n".to_string())
1650        );
1651    }
1652
1653    #[test]
1654    fn test_timestamp_parsing_errors() {
1655        let broker = BgpkitBroker::new();
1656
1657        // Invalid format - error occurs at query time
1658        let broker_with_invalid = broker.clone().ts_start("invalid-timestamp");
1659        let result = broker_with_invalid.query();
1660        assert!(result.is_err());
1661        assert!(matches!(
1662            result.err(),
1663            Some(BrokerError::ConfigurationError(_))
1664        ));
1665
1666        // Invalid date - error occurs at query time
1667        let broker_with_invalid = broker.clone().ts_end("2022-13-01");
1668        let result = broker_with_invalid.query();
1669        assert!(result.is_err());
1670        assert!(matches!(
1671            result.err(),
1672            Some(BrokerError::ConfigurationError(_))
1673        ));
1674
1675        // Invalid compact date - error occurs at query time
1676        let broker_with_invalid = broker.clone().ts_start("20221301");
1677        let result = broker_with_invalid.query();
1678        assert!(result.is_err());
1679        assert!(matches!(
1680            result.err(),
1681            Some(BrokerError::ConfigurationError(_))
1682        ));
1683
1684        // Partially valid format - error occurs at query time
1685        let broker_with_invalid = broker.clone().ts_start("2022-01");
1686        let result = broker_with_invalid.query();
1687        assert!(result.is_err());
1688        assert!(matches!(
1689            result.err(),
1690            Some(BrokerError::ConfigurationError(_))
1691        ));
1692    }
1693
1694    #[test]
1695    fn test_parse_timestamp_direct() {
1696        use chrono::{NaiveDate, NaiveDateTime};
1697
1698        // Test the parse_timestamp function directly - it now returns DateTime<Utc>
1699
1700        // Unix timestamp
1701        let expected_unix = Utc.timestamp_opt(1640995200, 0).single().unwrap();
1702        assert_eq!(
1703            BgpkitBroker::parse_timestamp("1640995200").unwrap(),
1704            expected_unix
1705        );
1706
1707        // RFC3339 formats
1708        let expected_rfc3339_z = Utc.from_utc_datetime(
1709            &NaiveDateTime::parse_from_str("2022-01-01T00:00:00", "%Y-%m-%dT%H:%M:%S").unwrap(),
1710        );
1711        assert_eq!(
1712            BgpkitBroker::parse_timestamp("2022-01-01T00:00:00Z").unwrap(),
1713            expected_rfc3339_z
1714        );
1715
1716        let expected_rfc3339_no_z = Utc.from_utc_datetime(
1717            &NaiveDateTime::parse_from_str("2022-01-01T12:30:45", "%Y-%m-%dT%H:%M:%S").unwrap(),
1718        );
1719        assert_eq!(
1720            BgpkitBroker::parse_timestamp("2022-01-01T12:30:45").unwrap(),
1721            expected_rfc3339_no_z
1722        );
1723
1724        let expected_space_format = Utc.from_utc_datetime(
1725            &NaiveDateTime::parse_from_str("2022-01-01 12:30:45", "%Y-%m-%d %H:%M:%S").unwrap(),
1726        );
1727        assert_eq!(
1728            BgpkitBroker::parse_timestamp("2022-01-01 12:30:45").unwrap(),
1729            expected_space_format
1730        );
1731
1732        // Pure date formats (all convert to start of day in UTC)
1733        let expected_date = Utc.from_utc_datetime(
1734            &NaiveDate::from_ymd_opt(2022, 1, 1)
1735                .unwrap()
1736                .and_hms_opt(0, 0, 0)
1737                .unwrap(),
1738        );
1739        assert_eq!(
1740            BgpkitBroker::parse_timestamp("2022-01-01").unwrap(),
1741            expected_date
1742        );
1743        assert_eq!(
1744            BgpkitBroker::parse_timestamp("2022/01/01").unwrap(),
1745            expected_date
1746        );
1747        assert_eq!(
1748            BgpkitBroker::parse_timestamp("2022.01.01").unwrap(),
1749            expected_date
1750        );
1751        assert_eq!(
1752            BgpkitBroker::parse_timestamp("20220101").unwrap(),
1753            expected_date
1754        );
1755
1756        // Test timezone formats - these should now work
1757        let result_plus_tz = BgpkitBroker::parse_timestamp("2022-01-01T00:00:00+00:00").unwrap();
1758        assert_eq!(result_plus_tz, expected_date);
1759        println!("✓ +00:00 timezone format works");
1760
1761        // Test timezone conversion: 2022-01-01T05:00:00-05:00 = 2022-01-01T10:00:00Z
1762        let result_minus_tz = BgpkitBroker::parse_timestamp("2022-01-01T05:00:00-05:00").unwrap();
1763        let expected_10am = Utc.with_ymd_and_hms(2022, 1, 1, 10, 0, 0).unwrap();
1764        assert_eq!(result_minus_tz, expected_10am);
1765        println!("✓ -05:00 timezone format works (05:00-05:00 = 10:00Z)");
1766
1767        // Error cases
1768        assert!(BgpkitBroker::parse_timestamp("invalid").is_err());
1769        assert!(BgpkitBroker::parse_timestamp("2022-13-01").is_err());
1770        assert!(BgpkitBroker::parse_timestamp("2022-01").is_err());
1771    }
1772
1773    #[test]
1774    fn test_collector_id_validation() {
1775        let broker = BgpkitBroker::new();
1776
1777        // Valid single collector - no error at validation time
1778        let broker_valid = broker.clone().collector_id("rrc00");
1779        let result = broker_valid.validate_configuration();
1780        assert!(result.is_ok());
1781
1782        // Valid multiple collectors - no error at validation time
1783        let broker_valid = broker.clone().collector_id("rrc00,route-views2");
1784        let result = broker_valid.validate_configuration();
1785        assert!(result.is_ok());
1786
1787        // Unknown collector should be allowed (permissive behavior)
1788        let broker_unknown = broker.clone().collector_id("brand-new-collector");
1789        let result = broker_unknown.validate_configuration();
1790        assert!(result.is_ok());
1791
1792        // Mixed known and unknown collectors should be allowed
1793        let broker_mixed = broker.clone().collector_id("rrc00,brand-new-collector");
1794        let result = broker_mixed.validate_configuration();
1795        assert!(result.is_ok());
1796
1797        // Empty/whitespace-only should error
1798        let broker_empty = broker.clone().collector_id(", ,  ,");
1799        let result = broker_empty.validate_configuration();
1800        assert!(result.is_err());
1801        assert!(matches!(
1802            result.err(),
1803            Some(BrokerError::ConfigurationError(_))
1804        ));
1805    }
1806
1807    #[test]
1808    fn test_project_validation() {
1809        let broker = BgpkitBroker::new();
1810
1811        // Valid projects - no error at configuration time
1812        let broker_valid = broker.clone().project("riperis");
1813        let result = broker_valid.validate_configuration();
1814        assert!(result.is_ok());
1815
1816        let broker_valid = broker.clone().project("routeviews");
1817        let result = broker_valid.validate_configuration();
1818        assert!(result.is_ok());
1819
1820        // Valid aliases - no error at configuration time
1821        let broker_valid = broker.clone().project("rrc");
1822        let result = broker_valid.validate_configuration();
1823        assert!(result.is_ok());
1824
1825        let broker_valid = broker.clone().project("rv");
1826        let result = broker_valid.validate_configuration();
1827        assert!(result.is_ok());
1828
1829        // Invalid project - error occurs at validation
1830        let broker_invalid = broker.clone().project("invalid-project");
1831        let result = broker_invalid.validate_configuration();
1832        assert!(result.is_err());
1833        assert!(matches!(
1834            result.err(),
1835            Some(BrokerError::ConfigurationError(_))
1836        ));
1837    }
1838
1839    #[test]
1840    fn test_data_type_validation() {
1841        let broker = BgpkitBroker::new();
1842
1843        // Valid data types - no error at configuration time
1844        let broker_valid = broker.clone().data_type("rib");
1845        let result = broker_valid.validate_configuration();
1846        assert!(result.is_ok());
1847
1848        let broker_valid = broker.clone().data_type("updates");
1849        let result = broker_valid.validate_configuration();
1850        assert!(result.is_ok());
1851
1852        // Valid aliases - no error at configuration time
1853        let broker_valid = broker.clone().data_type("ribs");
1854        let result = broker_valid.validate_configuration();
1855        assert!(result.is_ok());
1856
1857        let broker_valid = broker.clone().data_type("update");
1858        let result = broker_valid.validate_configuration();
1859        assert!(result.is_ok());
1860
1861        // Invalid data type - error occurs at validation
1862        let broker_invalid = broker.clone().data_type("invalid-type");
1863        let result = broker_invalid.validate_configuration();
1864        assert!(result.is_err());
1865        assert!(matches!(
1866            result.err(),
1867            Some(BrokerError::ConfigurationError(_))
1868        ));
1869    }
1870
1871    #[test]
1872    fn test_page_validation() {
1873        let broker = BgpkitBroker::new();
1874
1875        // Valid page number - no error at configuration time
1876        let broker_valid = broker.clone().page(1);
1877        let result = broker_valid.validate_configuration();
1878        assert!(result.is_ok());
1879
1880        let broker_valid = broker.clone().page(100);
1881        let result = broker_valid.validate_configuration();
1882        assert!(result.is_ok());
1883
1884        // Invalid page number - error occurs at validation
1885        let broker_invalid = broker.clone().page(0);
1886        let result = broker_invalid.validate_configuration();
1887        assert!(result.is_err());
1888        assert!(matches!(
1889            result.err(),
1890            Some(BrokerError::ConfigurationError(_))
1891        ));
1892    }
1893
1894    #[test]
1895    fn test_page_size_validation() {
1896        let broker = BgpkitBroker::new();
1897
1898        // Valid page sizes - no error at configuration time
1899        let broker_valid = broker.clone().page_size(1);
1900        let result = broker_valid.validate_configuration();
1901        assert!(result.is_ok());
1902
1903        let broker_valid = broker.clone().page_size(100);
1904        let result = broker_valid.validate_configuration();
1905        assert!(result.is_ok());
1906
1907        let broker_valid = broker.clone().page_size(100000);
1908        let result = broker_valid.validate_configuration();
1909        assert!(result.is_ok());
1910
1911        // Invalid page sizes - error occurs at validation
1912        let broker_invalid = broker.clone().page_size(0);
1913        let result = broker_invalid.validate_configuration();
1914        assert!(result.is_err());
1915        assert!(matches!(
1916            result.err(),
1917            Some(BrokerError::ConfigurationError(_))
1918        ));
1919
1920        let broker_invalid = broker.clone().page_size(100001);
1921        let result = broker_invalid.validate_configuration();
1922        assert!(result.is_err());
1923        assert!(matches!(
1924            result.err(),
1925            Some(BrokerError::ConfigurationError(_))
1926        ));
1927    }
1928
1929    #[test]
1930    fn test_method_chaining() {
1931        let broker = BgpkitBroker::new()
1932            .ts_start("1634693400")
1933            .ts_end("1634693400")
1934            .collector_id("rrc00")
1935            .project("riperis")
1936            .data_type("rib")
1937            .page(1)
1938            .page_size(10);
1939
1940        // Raw input is stored during configuration
1941        assert_eq!(broker.query_params.ts_start, Some("1634693400".to_string()));
1942        assert_eq!(broker.query_params.ts_end, Some("1634693400".to_string()));
1943        assert_eq!(broker.query_params.collector_id, Some("rrc00".to_string()));
1944        assert_eq!(broker.query_params.project, Some("riperis".to_string()));
1945        assert_eq!(broker.query_params.data_type, Some("rib".to_string()));
1946        assert_eq!(broker.query_params.page, 1);
1947        assert_eq!(broker.query_params.page_size, 10);
1948    }
1949}