Skip to content

Commit 2611f4a

Browse files
committed
Improve documentation and example error handling
- Update README version from 0.17 to 0.26 - Enhance examples with proper error handling using ? operator - Add comprehensive API documentation with usage examples
1 parent 5cd1133 commit 2611f4a

File tree

5 files changed

+229
-45
lines changed

5 files changed

+229
-45
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Add this to your `Cargo.toml`:
2828

2929
```toml
3030
[dependencies]
31-
maxminddb = "0.17"
31+
maxminddb = "0.26"
3232
```
3333

3434
and this to your crate root:

examples/lookup.rs

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,27 @@ use std::net::IpAddr;
22

33
use maxminddb::geoip2;
44

5-
fn main() -> Result<(), String> {
5+
fn main() -> Result<(), Box<dyn std::error::Error>> {
66
let mut args = std::env::args().skip(1);
7-
let reader = maxminddb::Reader::open_readfile(
8-
args.next()
9-
.ok_or("First argument must be the path to the IP database")?,
10-
)
11-
.unwrap();
12-
let ip: IpAddr = args
7+
let db_path = args
138
.next()
14-
.ok_or("Second argument must be the IP address, like 128.101.101.101")?
9+
.ok_or("First argument must be the path to the IP database")?;
10+
let reader = maxminddb::Reader::open_readfile(db_path)?;
11+
12+
let ip_str = args
13+
.next()
14+
.ok_or("Second argument must be the IP address, like 128.101.101.101")?;
15+
let ip: IpAddr = ip_str
1516
.parse()
16-
.unwrap();
17-
let city: Option<geoip2::City> = reader.lookup(ip).unwrap();
18-
println!("{city:#?}");
17+
.map_err(|e| format!("Invalid IP address '{}': {}", ip_str, e))?;
18+
19+
match reader.lookup::<geoip2::City>(ip)? {
20+
Some(city) => {
21+
println!("City data for IP {}: {city:#?}", ip);
22+
}
23+
None => {
24+
println!("No city data found for IP {}", ip);
25+
}
26+
}
1927
Ok(())
2028
}

examples/within.rs

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,25 @@
11
use ipnetwork::IpNetwork;
22
use maxminddb::{geoip2, Within};
33

4-
fn main() -> Result<(), String> {
4+
fn main() -> Result<(), Box<dyn std::error::Error>> {
55
let mut args = std::env::args().skip(1);
6-
let reader = maxminddb::Reader::open_readfile(
7-
args.next()
8-
.ok_or("First argument must be the path to the IP database")?,
9-
)
10-
.unwrap();
11-
let cidr: String = args
6+
let db_path = args
127
.next()
13-
.ok_or("Second argument must be the IP address and mask in CIDR notation, e.g. 0.0.0.0/0 or ::/0")?
8+
.ok_or("First argument must be the path to the IP database")?;
9+
let reader = maxminddb::Reader::open_readfile(db_path)?;
10+
11+
let cidr_str = args.next().ok_or(
12+
"Second argument must be the IP address and mask in CIDR notation, e.g. 0.0.0.0/0 or ::/0",
13+
)?;
14+
15+
let ip_net: IpNetwork = cidr_str
1416
.parse()
15-
.unwrap();
16-
let ip_net = if cidr.contains(':') {
17-
IpNetwork::V6(cidr.parse().unwrap())
18-
} else {
19-
IpNetwork::V4(cidr.parse().unwrap())
20-
};
17+
.map_err(|e| format!("Invalid CIDR notation '{}': {}", cidr_str, e))?;
2118

2219
let mut n = 0;
23-
let iter: Within<geoip2::City, _> = reader.within(ip_net).map_err(|e| e.to_string())?;
20+
let iter: Within<geoip2::City, _> = reader.within(ip_net)?;
2421
for next in iter {
25-
let item = next.map_err(|e| e.to_string())?;
22+
let item = next?;
2623
let continent = item.info.continent.and_then(|c| c.code).unwrap_or("");
2724
let country = item.info.country.and_then(|c| c.iso_code).unwrap_or("");
2825
let city = match item.info.city.and_then(|c| c.names) {
@@ -38,7 +35,7 @@ fn main() -> Result<(), String> {
3835
}
3936
n += 1;
4037
}
41-
eprintln!("processed {n} items");
38+
eprintln!("Processed {} items", n);
4239

4340
Ok(())
4441
}

src/maxminddb/geoip2.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,55 @@
1+
//! GeoIP2 and GeoLite2 database record structures
2+
//!
3+
//! This module provides strongly-typed Rust structures that correspond to the
4+
//! various GeoIP2 and GeoLite2 database record formats.
5+
//!
6+
//! # Record Types
7+
//!
8+
//! - [`City`] - Complete city-level geolocation data (most comprehensive)
9+
//! - [`Country`] - Country-level geolocation data
10+
//! - [`Enterprise`] - Enterprise database with additional confidence scores
11+
//! - [`Isp`] - Internet Service Provider information
12+
//! - [`AnonymousIp`] - Anonymous proxy and VPN detection
13+
//! - [`ConnectionType`] - Connection type classification
14+
//! - [`Domain`] - Domain information
15+
//! - [`Asn`] - Autonomous System Number data
16+
//! - [`DensityIncome`] - Population density and income data
17+
//!
18+
//! # Usage Examples
19+
//!
20+
//! ```rust
21+
//! use maxminddb::{Reader, geoip2};
22+
//! use std::net::IpAddr;
23+
//!
24+
//! # fn main() -> Result<(), maxminddb::MaxMindDbError> {
25+
//! let reader = Reader::open_readfile(
26+
//! "test-data/test-data/GeoIP2-City-Test.mmdb")?;
27+
//! let ip: IpAddr = "89.160.20.128".parse().unwrap();
28+
//!
29+
//! // City lookup (most common)
30+
//! if let Some(city) = reader.lookup::<geoip2::City>(ip)? {
31+
//! if let Some(city_names) = city.city.and_then(|c| c.names) {
32+
//! if let Some(city_name) = city_names.get("en") {
33+
//! println!("City: {}", city_name);
34+
//! }
35+
//! }
36+
//! if let Some(country_code) = city.country.and_then(|c| c.iso_code) {
37+
//! println!("Country: {}", country_code);
38+
//! }
39+
//! }
40+
//!
41+
//! // Country-only lookup (smaller/faster)
42+
//! if let Some(country) = reader.lookup::<geoip2::Country>(ip)? {
43+
//! if let Some(country_names) = country.country.and_then(|c| c.names) {
44+
//! if let Some(country_name) = country_names.get("en") {
45+
//! println!("Country: {}", country_name);
46+
//! }
47+
//! }
48+
//! }
49+
//! # Ok(())
50+
//! # }
51+
//! ```
52+
153
use serde::{Deserialize, Serialize};
254

355
/// GeoIP2 Country record

src/maxminddb/lib.rs

Lines changed: 143 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,57 @@
11
#![deny(trivial_casts, trivial_numeric_casts, unused_import_braces)]
2+
//! # MaxMind DB Reader
3+
//!
4+
//! This library reads the MaxMind DB format, including the GeoIP2 and GeoLite2 databases.
5+
//!
6+
//! ## Features
7+
//!
8+
//! This crate provides several optional features for performance and functionality:
9+
//!
10+
//! - **`mmap`** (default: disabled): Enable memory-mapped file access for
11+
//! better performance in long-running applications
12+
//! - **`simdutf8`** (default: disabled): Use SIMD instructions for faster
13+
//! UTF-8 validation during string decoding
14+
//! - **`unsafe-str-decode`** (default: disabled): Skip UTF-8 validation
15+
//! entirely for maximum performance (~20% faster lookups)
16+
//!
17+
//! **Note**: `simdutf8` and `unsafe-str-decode` are mutually exclusive.
18+
//!
19+
//! ## Database Compatibility
20+
//!
21+
//! This library supports all MaxMind DB format databases:
22+
//! - **GeoIP2** databases (City, Country, Enterprise, ISP, etc.)
23+
//! - **GeoLite2** databases (free versions)
24+
//! - Custom MaxMind DB format databases
25+
//!
26+
//! ## Thread Safety
27+
//!
28+
//! The `Reader` is `Send` and `Sync`, making it safe to share across threads.
29+
//! This makes it ideal for web servers and other concurrent applications.
30+
//!
31+
//! ## Quick Start
32+
//!
33+
//! ```rust
34+
//! use maxminddb::{Reader, geoip2};
35+
//! use std::net::IpAddr;
36+
//!
37+
//! fn main() -> Result<(), Box<dyn std::error::Error>> {
38+
//! // Open database file
39+
//! # let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb")?;
40+
//! # /*
41+
//! let reader = Reader::open_readfile("/path/to/GeoIP2-City.mmdb")?;
42+
//! # */
43+
//!
44+
//! // Look up an IP address
45+
//! let ip: IpAddr = "89.160.20.128".parse()?;
46+
//! if let Some(city) = reader.lookup::<geoip2::City>(ip)? {
47+
//! if let Some(country) = city.country {
48+
//! println!("Country: {}", country.iso_code.unwrap_or("Unknown"));
49+
//! }
50+
//! }
51+
//!
52+
//! Ok(())
53+
//! }
54+
//! ```
255
356
use std::cmp::Ordering;
457
use std::collections::BTreeMap;
@@ -200,7 +253,19 @@ impl<'de, T: Deserialize<'de>, S: AsRef<[u8]>> Iterator for Within<'de, T, S> {
200253
}
201254
}
202255

203-
/// A reader for the MaxMind DB format. The lifetime `'data` is tied to the lifetime of the underlying buffer holding the contents of the database file.
256+
/// A reader for the MaxMind DB format. The lifetime `'data` is tied to the
257+
/// lifetime of the underlying buffer holding the contents of the database file.
258+
///
259+
/// The `Reader` supports both file-based and memory-mapped access to MaxMind
260+
/// DB files, including GeoIP2 and GeoLite2 databases.
261+
///
262+
/// # Features
263+
///
264+
/// - **`mmap`**: Enable memory-mapped file access for better performance
265+
/// - **`simdutf8`**: Use SIMD-accelerated UTF-8 validation (faster string
266+
/// decoding)
267+
/// - **`unsafe-str-decode`**: Skip UTF-8 validation entirely (unsafe, but
268+
/// ~20% faster)
204269
#[derive(Debug)]
205270
pub struct Reader<S: AsRef<[u8]>> {
206271
buf: S,
@@ -234,7 +299,8 @@ impl Reader<Vec<u8>> {
234299
/// # Example
235300
///
236301
/// ```
237-
/// let reader = maxminddb::Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
302+
/// let reader = maxminddb::Reader::open_readfile(
303+
/// "test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
238304
/// ```
239305
pub fn open_readfile<P: AsRef<Path>>(database: P) -> Result<Reader<Vec<u8>>, MaxMindDbError> {
240306
let buf: Vec<u8> = fs::read(&database)?; // IO error converted via #[from]
@@ -275,24 +341,53 @@ impl<'de, S: AsRef<[u8]>> Reader<S> {
275341
/// Lookup the socket address in the opened MaxMind DB.
276342
/// Returns `Ok(None)` if the address is not found in the database.
277343
///
278-
/// Example:
344+
/// # Examples
279345
///
346+
/// Basic city lookup:
280347
/// ```
281348
/// # use maxminddb::geoip2;
282349
/// # use std::net::IpAddr;
283350
/// # use std::str::FromStr;
284351
/// # fn main() -> Result<(), maxminddb::MaxMindDbError> {
285-
/// let reader = maxminddb::Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb")?;
352+
/// let reader = maxminddb::Reader::open_readfile(
353+
/// "test-data/test-data/GeoIP2-City-Test.mmdb")?;
286354
///
287355
/// let ip: IpAddr = FromStr::from_str("89.160.20.128").unwrap();
288-
/// if let Some(city) = reader.lookup::<geoip2::City>(ip)? {
289-
/// println!("{:?}", city);
290-
/// } else {
291-
/// println!("Address not found");
356+
/// match reader.lookup::<geoip2::City>(ip)? {
357+
/// Some(city) => {
358+
/// if let Some(city_names) = city.city.and_then(|c| c.names) {
359+
/// if let Some(name) = city_names.get("en") {
360+
/// println!("City: {}", name);
361+
/// }
362+
/// }
363+
/// if let Some(country) = city.country.and_then(|c| c.iso_code) {
364+
/// println!("Country: {}", country);
365+
/// }
366+
/// }
367+
/// None => println!("No data found for IP {}", ip),
292368
/// }
293369
/// # Ok(())
294370
/// # }
295371
/// ```
372+
///
373+
/// Lookup with different record types:
374+
/// ```
375+
/// # use maxminddb::geoip2;
376+
/// # use std::net::IpAddr;
377+
/// # fn main() -> Result<(), maxminddb::MaxMindDbError> {
378+
/// let reader = maxminddb::Reader::open_readfile(
379+
/// "test-data/test-data/GeoIP2-City-Test.mmdb")?;
380+
/// let ip: IpAddr = "89.160.20.128".parse().unwrap();
381+
///
382+
/// // Different record types for the same IP
383+
/// let city: Option<geoip2::City> = reader.lookup(ip)?;
384+
/// let country: Option<geoip2::Country> = reader.lookup(ip)?;
385+
///
386+
/// println!("City data available: {}", city.is_some());
387+
/// println!("Country data available: {}", country.is_some());
388+
/// # Ok(())
389+
/// # }
390+
/// ```
296391
pub fn lookup<T>(&'de self, address: IpAddr) -> Result<Option<T>, MaxMindDbError>
297392
where
298393
T: Deserialize<'de>,
@@ -314,7 +409,8 @@ impl<'de, S: AsRef<[u8]>> Reader<S> {
314409
/// # use std::net::IpAddr;
315410
/// # use std::str::FromStr;
316411
/// # fn main() -> Result<(), maxminddb::MaxMindDbError> {
317-
/// let reader = maxminddb::Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb")?;
412+
/// let reader = maxminddb::Reader::open_readfile(
413+
/// "test-data/test-data/GeoIP2-City-Test.mmdb")?;
318414
///
319415
/// let ip: IpAddr = "89.160.20.128".parse().unwrap(); // Known IP
320416
/// let ip_unknown: IpAddr = "10.0.0.1".parse().unwrap(); // Unknown IP
@@ -359,19 +455,50 @@ impl<'de, S: AsRef<[u8]>> Reader<S> {
359455

360456
/// Iterate over blocks of IP networks in the opened MaxMind DB
361457
///
362-
/// Example:
458+
/// This method returns an iterator that yields all IP network blocks that
459+
/// fall within the specified CIDR range and have associated data in the
460+
/// database.
461+
///
462+
/// # Examples
363463
///
464+
/// Iterate over all IPv4 networks:
364465
/// ```
365466
/// use ipnetwork::IpNetwork;
366467
/// use maxminddb::{geoip2, Within};
367468
///
368-
/// let reader = maxminddb::Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
469+
/// let reader = maxminddb::Reader::open_readfile(
470+
/// "test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
471+
///
472+
/// let ipv4_all = IpNetwork::V4("0.0.0.0/0".parse().unwrap());
473+
/// let mut count = 0;
474+
/// for result in reader.within::<geoip2::City>(ipv4_all).unwrap() {
475+
/// let item = result.unwrap();
476+
/// let city_name = item.info.city.as_ref().and_then(|c| c.names.as_ref()).and_then(|n| n.get("en"));
477+
/// println!("Network: {}, City: {:?}", item.ip_net, city_name);
478+
/// count += 1;
479+
/// if count >= 10 { break; } // Limit output for example
480+
/// }
481+
/// ```
482+
///
483+
/// Search within a specific subnet:
484+
/// ```
485+
/// use ipnetwork::IpNetwork;
486+
/// use maxminddb::geoip2;
487+
///
488+
/// let reader = maxminddb::Reader::open_readfile(
489+
/// "test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
369490
///
370-
/// let ip_net = IpNetwork::V6("::/0".parse().unwrap());
371-
/// let mut iter: Within<geoip2::City, _> = reader.within(ip_net).unwrap();
372-
/// while let Some(next) = iter.next() {
373-
/// let item = next.unwrap();
374-
/// println!("ip_net={}, city={:?}", item.ip_net, item.info);
491+
/// let subnet = IpNetwork::V4("192.168.0.0/16".parse().unwrap());
492+
/// match reader.within::<geoip2::City>(subnet) {
493+
/// Ok(iter) => {
494+
/// for result in iter {
495+
/// match result {
496+
/// Ok(item) => println!("Found: {}", item.ip_net),
497+
/// Err(e) => eprintln!("Error processing item: {}", e),
498+
/// }
499+
/// }
500+
/// }
501+
/// Err(e) => eprintln!("Failed to create iterator: {}", e),
375502
/// }
376503
/// ```
377504
pub fn within<T>(&'de self, cidr: IpNetwork) -> Result<Within<'de, T, S>, MaxMindDbError>

0 commit comments

Comments
 (0)