Skip to main content

bgpkit_commons/
lib.rs

1//! # Overview
2//!
3//! `bgpkit-commons` is a library for common BGP-related data and functions with a lazy-loading
4//! architecture. Each module can be independently enabled via feature flags, allowing for minimal builds.
5//!
6//! ## Quick Start
7//!
8//! Add `bgpkit-commons` to your `Cargo.toml`:
9//!
10//! ```toml
11//! [dependencies]
12//! bgpkit-commons = "0.10"
13//! ```
14//!
15//! All modules follow the same pattern: create a [`BgpkitCommons`] instance, call a `load_xxx()`
16//! method to fetch data, then use `xxx_yyy()` methods to access it.
17//!
18//! ```rust
19//! # #[cfg(feature = "bogons")]
20//! # fn main() {
21//! use bgpkit_commons::BgpkitCommons;
22//!
23//! let mut commons = BgpkitCommons::new();
24//! commons.load_bogons().unwrap();
25//!
26//! if let Ok(is_bogon) = commons.bogons_match("23456") {
27//!     println!("ASN 23456 is a bogon: {}", is_bogon);
28//! }
29//! # }
30//! # #[cfg(not(feature = "bogons"))]
31//! # fn main() {}
32//! ```
33//!
34//! ## Modules
35//!
36//! ### [`asinfo`] — Autonomous System Information
37//!
38//! Feature: `asinfo` | Sources: RIPE NCC, CAIDA as2org, APNIC population, IIJ IHR hegemony, PeeringDB
39//!
40//! - Load: `load_asinfo(as2org, population, hegemony, peeringdb)`, `load_asinfo_cached()`, `load_asinfo_with(builder)`
41//! - Access: `asinfo_get(asn)`, `asinfo_all()`, `asinfo_are_siblings(asn1, asn2)`
42//! - AS name resolution, country mapping, organization data, population statistics, hegemony scores
43//!
44//! ### [`as2rel`] — AS Relationship Data
45//!
46//! Feature: `as2rel` | Source: BGPKIT AS relationship inference
47//!
48//! - Load: `load_as2rel()`
49//! - Access: `as2rel_lookup(asn1, asn2)`
50//! - Provider-customer, peer-to-peer, and sibling relationships between ASes
51//!
52//! ### [`bogons`] — Bogon Detection
53//!
54//! Feature: `bogons` | Source: IANA special registries (IPv4, IPv6, ASN)
55//!
56//! - Load: `load_bogons()`
57//! - Access: `bogons_match(input)`, `bogons_match_prefix(prefix)`, `bogons_match_asn(asn)`, `get_bogon_prefixes()`, `get_bogon_asns()`
58//! - Detect invalid/reserved IP prefixes and ASNs that shouldn't appear in routing
59//!
60//! ### [`countries`] — Country Information
61//!
62//! Feature: `countries` | Source: GeoNames geographical database
63//!
64//! - Load: `load_countries()`
65//! - Access: `country_by_code(code)`, `country_by_code3(code)`, `country_by_name(name)`, `country_all()`
66//! - ISO country code to name mapping and geographical information
67//!
68//! ### [`mrt_collectors`] — MRT Collector Metadata
69//!
70//! Feature: `mrt_collectors` | Sources: RouteViews and RIPE RIS official APIs
71//!
72//! - Load: `load_mrt_collectors()`, `load_mrt_collector_peers()`
73//! - Access: `mrt_collectors_all()`, `mrt_collectors_by_name(name)`, `mrt_collectors_by_country(country)`, `mrt_collector_peers_all()`, `mrt_collector_peers_full_feed()`
74//! - BGP collector information, peer details, full-feed vs partial-feed classification
75//!
76//! ### [`peeringdb`] — PeeringDB Data
77//!
78//! Feature: `peeringdb` | Source: [PeeringDB API](https://www.peeringdb.com/api/)
79//!
80//! - Load: `Peeringdb::new()` (all tables), `Peeringdb::new_networks_only()` (lightweight)
81//! - Access: `get_network(asn)`, `get_ixp(ix_id)`, `get_ixp_memberships(asn)`, `lookup_ixp_prefix(prefix)`, `get_facility(fac_id)`
82//! - Typed structs mirroring all 12 PeeringDB API endpoints: networks, internet exchanges,
83//!   IXP prefixes, IXP membership, facilities, organizations, carriers, and more
84//! - `PeeringdbData` is a type alias for the full `Network` struct (backward compatible)
85//!
86//! ### [`rpki`] — RPKI Validation
87//!
88//! Feature: `rpki` | Sources: Cloudflare (real-time), RIPE NCC historical, RPKIviews historical, RPKISPOOL historical
89//!
90//! - Load: `load_rpki(optional_date)`, `load_rpki_historical(date, source)`, `load_rpki_from_files(urls, source, date)`
91//! - Poll: `RpkiTrie::from_cloudflare_conditional(etag, last_modified)` returns `Ok(None)` on `304 Not Modified`
92//! - Access: `rpki_validate(asn, prefix)`, `rpki_validate_check_expiry(asn, prefix, timestamp)`, `rpki_lookup_by_prefix(prefix)`, `rpki_lookup_aspa(customer_asn)`
93//! - Route Origin Authorization (ROA) and ASPA validation, supports real-time and historical sources
94//! - Poll current Cloudflare data with `RpkiTrie::from_cloudflare_conditional`, retaining the returned
95//!   [`rpki::RpkiLoad`] validators and keeping the existing trie when the result is `Ok(None)`.
96//! - `BgpkitCommons::reload()` performs a full reload; it does not use validators or provide an atomic
97//!   poll-and-swap operation.
98//!
99//! ## Examples
100//!
101//! ### Loading multiple modules
102//!
103//! ```rust
104//! # #[cfg(all(feature = "asinfo", feature = "countries"))]
105//! # fn main() {
106//! use bgpkit_commons::BgpkitCommons;
107//!
108//! let mut commons = BgpkitCommons::new();
109//! commons.load_asinfo_with_profile(Default::default()).unwrap();
110//! commons.load_countries().unwrap();
111//!
112//! if let Ok(Some(asinfo)) = commons.asinfo_get(13335) {
113//!     println!("AS13335: {} ({})", asinfo.name, asinfo.country);
114//! }
115//! # }
116//! # #[cfg(not(all(feature = "asinfo", feature = "countries")))]
117//! # fn main() {}
118//! ```
119//!
120//! ### Using AsInfoBuilder
121//!
122//! ```rust
123//! # #[cfg(feature = "asinfo")]
124//! # fn main() {
125//! use bgpkit_commons::BgpkitCommons;
126//!
127//! let mut commons = BgpkitCommons::new();
128//! let builder = commons.asinfo_builder()
129//!     .with_as2org()
130//!     .with_peeringdb();
131//! commons.load_asinfo_with(builder).unwrap();
132//!
133//! if let Ok(are_siblings) = commons.asinfo_are_siblings(13335, 132892) {
134//!     println!("AS13335 and AS132892 are siblings: {}", are_siblings);
135//! }
136//! # }
137//! # #[cfg(not(feature = "asinfo"))]
138//! # fn main() {}
139//! ```
140//!
141//! ### Loading historical RPKI data
142//!
143//! ```rust,no_run
144//! # #[cfg(feature = "rpki")]
145//! # fn main() {
146//! use bgpkit_commons::BgpkitCommons;
147//! use bgpkit_commons::rpki::{HistoricalRpkiSource, RpkiViewsCollector};
148//! use chrono::NaiveDate;
149//!
150//! let mut commons = BgpkitCommons::new();
151//! let date = NaiveDate::from_ymd_opt(2024, 1, 4).unwrap();
152//!
153//! // Load from RIPE NCC historical archives
154//! commons.load_rpki_historical(date, HistoricalRpkiSource::Ripe).unwrap();
155//!
156//! // Or load from RPKIviews collectors
157//! let source = HistoricalRpkiSource::RpkiViews(RpkiViewsCollector::KerfuffleNet);
158//! commons.load_rpki_historical(date, source).unwrap();
159//!
160//! // List available files for a date
161//! let files = commons.list_rpki_files(date, HistoricalRpkiSource::Ripe).unwrap();
162//! # }
163//! # #[cfg(not(feature = "rpki"))]
164//! # fn main() {}
165//! ```
166//!
167//! ### Direct module access
168//!
169//! Modules can also be used directly without `BgpkitCommons`:
170//!
171//! ```rust
172//! # #[cfg(feature = "bogons")]
173//! # fn main() {
174//! use bgpkit_commons::bogons::Bogons;
175//! let bogons = Bogons::new().unwrap();
176//! # }
177//! # #[cfg(not(feature = "bogons"))]
178//! # fn main() {}
179//! ```
180//!
181//! ## Feature Flags
182//!
183//! | Feature | Description |
184//! |---------|-------------|
185//! | `asinfo` | AS information: names, countries, organizations, population, hegemony |
186//! | `as2rel` | AS relationship data |
187//! | `bogons` | Bogon prefix and ASN detection |
188//! | `countries` | Country information lookup |
189//! | `mrt_collectors` | MRT collector metadata |
190//! | `peeringdb` | PeeringDB API data (networks, IXPs, facilities, organizations) |
191//! | `rpki` | RPKI validation (ROA and ASPA) |
192//! | `all` *(default)* | Enables all modules |
193//!
194//! For a minimal build:
195//!
196//! ```toml
197//! [dependencies]
198//! bgpkit-commons = { version = "0.13", default-features = false, features = ["bogons", "countries"] }
199//! ```
200
201#![doc(
202    html_logo_url = "https://raw.githubusercontent.com/bgpkit/assets/main/logos/icon-transparent.png",
203    html_favicon_url = "https://raw.githubusercontent.com/bgpkit/assets/main/logos/favicon.ico"
204)]
205
206#[cfg(feature = "as2rel")]
207pub mod as2rel;
208#[cfg(feature = "asinfo")]
209pub mod asinfo;
210#[cfg(feature = "bogons")]
211pub mod bogons;
212#[cfg(feature = "countries")]
213pub mod countries;
214#[cfg(feature = "irr")]
215pub mod irr;
216
217#[cfg(feature = "delegated")]
218pub mod delegated;
219#[cfg(feature = "mrt_collectors")]
220pub mod mrt_collectors;
221#[cfg(feature = "peeringdb")]
222pub mod peeringdb;
223#[cfg(feature = "rpki")]
224pub mod rpki;
225
226#[cfg(feature = "export")]
227pub mod export;
228
229pub mod errors;
230
231// Re-export error types for convenience
232pub use errors::{BgpkitCommonsError, Result};
233
234/// Trait for modules that support lazy loading and reloading of data
235pub trait LazyLoadable {
236    /// Reload the module's data from its external sources
237    fn reload(&mut self) -> Result<()>;
238
239    /// Check if the module's data is currently loaded
240    fn is_loaded(&self) -> bool;
241
242    /// Get a description of the module's current loading status
243    fn loading_status(&self) -> &'static str;
244}
245
246#[derive(Default)]
247pub struct BgpkitCommons {
248    #[cfg(feature = "countries")]
249    countries: Option<crate::countries::Countries>,
250    #[cfg(feature = "rpki")]
251    rpki_trie: Option<crate::rpki::RpkiTrie>,
252    #[cfg(feature = "mrt_collectors")]
253    mrt_collectors: Option<Vec<crate::mrt_collectors::MrtCollector>>,
254    #[cfg(feature = "mrt_collectors")]
255    mrt_collector_peers: Option<Vec<crate::mrt_collectors::MrtCollectorPeer>>,
256    #[cfg(feature = "bogons")]
257    bogons: Option<crate::bogons::Bogons>,
258    #[cfg(feature = "asinfo")]
259    asinfo: Option<crate::asinfo::AsInfoUtils>,
260    #[cfg(feature = "as2rel")]
261    as2rel: Option<crate::as2rel::As2relBgpkit>,
262}
263
264impl BgpkitCommons {
265    pub fn new() -> Self {
266        Self::default()
267    }
268
269    /// Reload all data sources that are already loaded
270    pub fn reload(&mut self) -> Result<()> {
271        #[cfg(feature = "countries")]
272        if self.countries.is_some() {
273            self.load_countries()?;
274        }
275        #[cfg(feature = "rpki")]
276        if let Some(rpki) = self.rpki_trie.as_mut() {
277            rpki.reload()?;
278        }
279        #[cfg(feature = "mrt_collectors")]
280        if self.mrt_collectors.is_some() {
281            self.load_mrt_collectors()?;
282        }
283        #[cfg(feature = "mrt_collectors")]
284        if self.mrt_collector_peers.is_some() {
285            self.load_mrt_collector_peers()?;
286        }
287        #[cfg(feature = "bogons")]
288        if self.bogons.is_some() {
289            self.load_bogons()?;
290        }
291        #[cfg(feature = "asinfo")]
292        if let Some(asinfo) = self.asinfo.as_mut() {
293            asinfo.reload()?;
294        }
295        #[cfg(feature = "as2rel")]
296        if self.as2rel.is_some() {
297            self.load_as2rel()?;
298        }
299
300        Ok(())
301    }
302
303    /// Get loading status for all available modules
304    pub fn loading_status(&self) -> Vec<(&'static str, &'static str)> {
305        #[allow(unused_mut)] // mut needed when any features are enabled
306        let mut status = Vec::new();
307
308        #[cfg(feature = "countries")]
309        if let Some(ref countries) = self.countries {
310            status.push(("countries", countries.loading_status()));
311        } else {
312            status.push(("countries", "Countries data not loaded"));
313        }
314
315        #[cfg(feature = "bogons")]
316        if let Some(ref bogons) = self.bogons {
317            status.push(("bogons", bogons.loading_status()));
318        } else {
319            status.push(("bogons", "Bogons data not loaded"));
320        }
321
322        #[cfg(feature = "rpki")]
323        if let Some(ref rpki) = self.rpki_trie {
324            status.push(("rpki", rpki.loading_status()));
325        } else {
326            status.push(("rpki", "RPKI data not loaded"));
327        }
328
329        #[cfg(feature = "asinfo")]
330        if let Some(ref asinfo) = self.asinfo {
331            status.push(("asinfo", asinfo.loading_status()));
332        } else {
333            status.push(("asinfo", "ASInfo data not loaded"));
334        }
335
336        #[cfg(feature = "as2rel")]
337        if let Some(ref as2rel) = self.as2rel {
338            status.push(("as2rel", as2rel.loading_status()));
339        } else {
340            status.push(("as2rel", "AS2Rel data not loaded"));
341        }
342
343        #[cfg(feature = "mrt_collectors")]
344        {
345            if self.mrt_collectors.is_some() {
346                status.push(("mrt_collectors", "MRT collectors data loaded"));
347            } else {
348                status.push(("mrt_collectors", "MRT collectors data not loaded"));
349            }
350
351            if self.mrt_collector_peers.is_some() {
352                status.push(("mrt_collector_peers", "MRT collector peers data loaded"));
353            } else {
354                status.push(("mrt_collector_peers", "MRT collector peers data not loaded"));
355            }
356        }
357
358        status
359    }
360
361    /// Load countries data
362    #[cfg(feature = "countries")]
363    pub fn load_countries(&mut self) -> Result<()> {
364        self.countries = Some(crate::countries::Countries::new()?);
365        Ok(())
366    }
367
368    /// Load RPKI data from Cloudflare (real-time) or historical archives
369    ///
370    /// - If `date_opt` is `None`, loads real-time data from Cloudflare
371    /// - If `date_opt` is `Some(date)`, loads historical data from RIPE NCC by default
372    ///
373    /// For more control over the data source, use `load_rpki_historical()` instead.
374    #[cfg(feature = "rpki")]
375    pub fn load_rpki(&mut self, date_opt: Option<chrono::NaiveDate>) -> Result<()> {
376        if let Some(date) = date_opt {
377            self.rpki_trie = Some(rpki::RpkiTrie::from_ripe_historical(date)?);
378        } else {
379            self.rpki_trie = Some(rpki::RpkiTrie::from_cloudflare()?);
380        }
381        Ok(())
382    }
383
384    /// Load RPKI data from a specific historical data source
385    ///
386    /// This allows you to choose between RIPE NCC and RPKIviews for historical data.
387    ///
388    /// # Example
389    ///
390    /// ```rust,no_run
391    /// use bgpkit_commons::BgpkitCommons;
392    /// use bgpkit_commons::rpki::{HistoricalRpkiSource, RpkiViewsCollector};
393    /// use chrono::NaiveDate;
394    ///
395    /// let mut commons = BgpkitCommons::new();
396    /// let date = NaiveDate::from_ymd_opt(2024, 1, 4).unwrap();
397    ///
398    /// // Load from RIPE NCC
399    /// commons.load_rpki_historical(date, HistoricalRpkiSource::Ripe).unwrap();
400    ///
401    /// // Or load from RPKIviews
402    /// let source = HistoricalRpkiSource::RpkiViews(RpkiViewsCollector::KerfuffleNet);
403    /// commons.load_rpki_historical(date, source).unwrap();
404    /// ```
405    #[cfg(feature = "rpki")]
406    pub fn load_rpki_historical(
407        &mut self,
408        date: chrono::NaiveDate,
409        source: rpki::HistoricalRpkiSource,
410    ) -> Result<()> {
411        match source {
412            rpki::HistoricalRpkiSource::Ripe => {
413                self.rpki_trie = Some(rpki::RpkiTrie::from_ripe_historical(date)?);
414            }
415            rpki::HistoricalRpkiSource::RpkiViews(collector) => {
416                self.rpki_trie = Some(rpki::RpkiTrie::from_rpkiviews(collector, date)?);
417            }
418            rpki::HistoricalRpkiSource::RpkiSpools(collector) => {
419                self.rpki_trie = Some(rpki::RpkiTrie::from_rpkispools(collector, date)?);
420            }
421        }
422        Ok(())
423    }
424
425    /// Load RPKI data from specific file URLs
426    ///
427    /// This allows loading from specific archive files, which is useful when you want
428    /// to process multiple files or use specific timestamps.
429    ///
430    /// # Arguments
431    ///
432    /// * `urls` - A slice of URLs pointing to RPKI data files
433    /// * `source` - The type of data source (RIPE, RPKIviews, or RPKISPOOL) - determines how files are parsed
434    /// * `date` - Optional date to associate with the loaded data
435    ///
436    /// # Example
437    ///
438    /// ```rust,no_run
439    /// use bgpkit_commons::BgpkitCommons;
440    /// use bgpkit_commons::rpki::HistoricalRpkiSource;
441    ///
442    /// let mut commons = BgpkitCommons::new();
443    /// let urls = vec![
444    ///     "https://example.com/rpki-20240104T144128Z.tgz".to_string(),
445    /// ];
446    /// commons.load_rpki_from_files(&urls, HistoricalRpkiSource::RpkiViews(
447    ///     bgpkit_commons::rpki::RpkiViewsCollector::KerfuffleNet
448    /// ), None).unwrap();
449    /// ```
450    #[cfg(feature = "rpki")]
451    pub fn load_rpki_from_files(
452        &mut self,
453        urls: &[String],
454        source: rpki::HistoricalRpkiSource,
455        date: Option<chrono::NaiveDate>,
456    ) -> Result<()> {
457        match source {
458            rpki::HistoricalRpkiSource::Ripe => {
459                self.rpki_trie = Some(rpki::RpkiTrie::from_ripe_files(urls, date)?);
460            }
461            rpki::HistoricalRpkiSource::RpkiViews(_) => {
462                self.rpki_trie = Some(rpki::RpkiTrie::from_rpkiviews_files(urls, date)?);
463            }
464            rpki::HistoricalRpkiSource::RpkiSpools(_) => {
465                // For RPKISPOOL, each URL is a tar.zst archive; load the first one
466                if let Some(url) = urls.first() {
467                    self.rpki_trie = Some(rpki::RpkiTrie::from_rpkispools_url(url, date)?);
468                }
469            }
470        }
471        Ok(())
472    }
473
474    /// List available RPKI files for a given date from a specific source
475    ///
476    /// # Example
477    ///
478    /// ```rust,no_run
479    /// use bgpkit_commons::BgpkitCommons;
480    /// use bgpkit_commons::rpki::{HistoricalRpkiSource, RpkiViewsCollector};
481    /// use chrono::NaiveDate;
482    ///
483    /// let commons = BgpkitCommons::new();
484    /// let date = NaiveDate::from_ymd_opt(2024, 1, 4).unwrap();
485    ///
486    /// // List files from RIPE NCC
487    /// let ripe_files = commons.list_rpki_files(date, HistoricalRpkiSource::Ripe).unwrap();
488    ///
489    /// // List files from RPKIviews
490    /// let source = HistoricalRpkiSource::RpkiViews(RpkiViewsCollector::KerfuffleNet);
491    /// let rpkiviews_files = commons.list_rpki_files(date, source).unwrap();
492    /// ```
493    #[cfg(feature = "rpki")]
494    pub fn list_rpki_files(
495        &self,
496        date: chrono::NaiveDate,
497        source: rpki::HistoricalRpkiSource,
498    ) -> Result<Vec<rpki::RpkiFile>> {
499        match source {
500            rpki::HistoricalRpkiSource::Ripe => rpki::list_ripe_files(date),
501            rpki::HistoricalRpkiSource::RpkiViews(collector) => {
502                rpki::list_rpkiviews_files(collector, date)
503            }
504            rpki::HistoricalRpkiSource::RpkiSpools(collector) => {
505                rpki::list_rpkispools_files(collector, date)
506            }
507        }
508    }
509
510    /// Load MRT mrt_collectors data
511    #[cfg(feature = "mrt_collectors")]
512    pub fn load_mrt_collectors(&mut self) -> Result<()> {
513        self.mrt_collectors = Some(crate::mrt_collectors::get_all_collectors()?);
514        Ok(())
515    }
516
517    /// Load MRT mrt_collectors data
518    #[cfg(feature = "mrt_collectors")]
519    pub fn load_mrt_collector_peers(&mut self) -> Result<()> {
520        self.mrt_collector_peers = Some(crate::mrt_collectors::get_mrt_collector_peers()?);
521        Ok(())
522    }
523
524    /// Load bogons data
525    #[cfg(feature = "bogons")]
526    pub fn load_bogons(&mut self) -> Result<()> {
527        self.bogons = Some(crate::bogons::Bogons::new()?);
528        Ok(())
529    }
530
531    /// Load AS information using a loading profile.
532    ///
533    /// Profiles provide curated presets for common use cases:
534    /// - [`Minimum`](crate::asinfo::AsInfoProfile::Minimum): asn.txt only (~1s)
535    /// - [`Default`](crate::asinfo::AsInfoProfile::Default): + as2org, population, hegemony, peeringdb (~30s)
536    /// - [`Full`](crate::asinfo::AsInfoProfile::Full): + delegated stats, IRR data with route prefixes (~75s)
537    ///
538    /// For fine-grained control beyond these presets, use the builder via
539    /// [`BgpkitCommons::asinfo_builder`].
540    ///
541    /// # Example
542    ///
543    /// ```rust,no_run
544    /// use bgpkit_commons::asinfo::AsInfoProfile;
545    /// use bgpkit_commons::BgpkitCommons;
546    ///
547    /// let mut commons = BgpkitCommons::new();
548    /// commons.load_asinfo_with_profile(AsInfoProfile::Default).unwrap();
549    /// ```
550    #[cfg(feature = "asinfo")]
551    pub fn load_asinfo_with_profile(
552        &mut self,
553        profile: crate::asinfo::AsInfoProfile,
554    ) -> Result<()> {
555        self.asinfo = Some(profile.builder().build()?);
556        Ok(())
557    }
558
559    /// Load AS name and country data with optional enrichment sources.
560    ///
561    /// Compatibility shim retained from the pre-v0.12 boolean-argument API.
562    /// Maps to the equivalent [`AsInfoBuilder`](crate::asinfo::AsInfoBuilder)
563    /// configuration with identical behavior. Prefer
564    /// [`BgpkitCommons::load_asinfo_with_profile`] or
565    /// [`BgpkitCommons::load_asinfo_with`].
566    #[cfg(feature = "asinfo")]
567    #[deprecated(note = "use load_asinfo_with_profile() or load_asinfo_with() instead")]
568    pub fn load_asinfo(
569        &mut self,
570        load_as2org: bool,
571        load_population: bool,
572        load_hegemony: bool,
573        load_peeringdb: bool,
574    ) -> Result<()> {
575        let mut builder = crate::asinfo::AsInfoBuilder::new();
576        if load_as2org {
577            builder = builder.with_as2org();
578        }
579        if load_population {
580            builder = builder.with_population();
581        }
582        if load_hegemony {
583            builder = builder.with_hegemony();
584        }
585        if load_peeringdb {
586            builder = builder.with_peeringdb();
587        }
588        self.asinfo = Some(builder.build()?);
589        Ok(())
590    }
591
592    #[cfg(feature = "asinfo")]
593    pub fn load_asinfo_cached(&mut self) -> Result<()> {
594        self.asinfo = Some(crate::asinfo::AsInfoUtils::new_from_cached()?);
595        Ok(())
596    }
597
598    /// Returns a builder for loading AS information with specific data sources.
599    ///
600    /// This provides a more ergonomic way to configure which data sources to load
601    /// compared to the `load_asinfo()` method with boolean parameters.
602    ///
603    /// # Example
604    ///
605    /// ```rust,no_run
606    /// use bgpkit_commons::BgpkitCommons;
607    ///
608    /// let mut commons = BgpkitCommons::new();
609    /// let builder = commons.asinfo_builder()
610    ///     .with_as2org()
611    ///     .with_peeringdb();
612    /// commons.load_asinfo_with(builder).unwrap();
613    /// ```
614    #[cfg(feature = "asinfo")]
615    pub fn asinfo_builder(&self) -> crate::asinfo::AsInfoBuilder {
616        crate::asinfo::AsInfoBuilder::new()
617    }
618
619    /// Load AS information using a pre-configured builder.
620    ///
621    /// # Example
622    ///
623    /// ```rust,no_run
624    /// use bgpkit_commons::BgpkitCommons;
625    ///
626    /// let mut commons = BgpkitCommons::new();
627    /// let builder = commons.asinfo_builder()
628    ///     .with_as2org()
629    ///     .with_hegemony();
630    /// commons.load_asinfo_with(builder).unwrap();
631    /// ```
632    #[cfg(feature = "asinfo")]
633    pub fn load_asinfo_with(&mut self, builder: crate::asinfo::AsInfoBuilder) -> Result<()> {
634        self.asinfo = Some(builder.build()?);
635        Ok(())
636    }
637
638    /// Load AS-level relationship data
639    #[cfg(feature = "as2rel")]
640    pub fn load_as2rel(&mut self) -> Result<()> {
641        self.as2rel = Some(crate::as2rel::As2relBgpkit::new()?);
642        Ok(())
643    }
644}