modo-rs 0.8.0

Rust web framework for small monolithic apps
Documentation
# modo::geolocation

IP-to-location lookup using a MaxMind GeoLite2/GeoIP2 `.mmdb` database.

## Key Types

| Type                | Description                                                                 |
| ------------------- | --------------------------------------------------------------------------- |
| `GeolocationConfig` | Config struct; deserializes from the `geolocation` YAML section             |
| `GeoLocator`        | Reads the `.mmdb` file and performs IP lookups; cheaply cloneable via `Arc` |
| `GeoLayer`          | Tower layer; runs lookup per request and inserts `Location` in extensions   |
| `Location`          | Resolved geolocation data; also an axum extractor                           |

## MaxMind database setup

This module reads a MaxMind `.mmdb` database (GeoLite2-City or GeoIP2-City).
The framework does not ship a database — obtain one from MaxMind:

1. Sign up for a free [MaxMind]https://www.maxmind.com account and generate
   a license key to download the GeoLite2-City database, or purchase a
   commercial GeoIP2-City database.
2. Place the `.mmdb` file somewhere the process can read at startup, e.g.
   `data/GeoLite2-City.mmdb`.
3. Keep the file current — MaxMind publishes weekly updates.

For tests, the repository ships `tests/fixtures/GeoIP2-City-Test.mmdb`, the
MaxMind-provided sample database used by the in-crate test suite.

## Configuration

Add a `geolocation` section to your application YAML config:

```yaml
geolocation:
    mmdb_path: data/GeoLite2-City.mmdb
```

`mmdb_path` supports `${VAR}` and `${VAR:default}` env-var substitution:

```yaml
geolocation:
    mmdb_path: ${MMDB_PATH:data/GeoLite2-City.mmdb}
```

## Usage

### Building the locator

```rust,ignore
use modo::geolocation::{GeoLocator, GeolocationConfig};

fn build_locator() -> modo::Result<GeoLocator> {
    let config = GeolocationConfig {
        mmdb_path: "data/GeoLite2-City.mmdb".to_string(),
    };
    GeoLocator::from_config(&config)
}
```

Returns an error when `mmdb_path` is empty or the file cannot be opened.

### Direct lookup

```rust,ignore
use std::net::IpAddr;
use modo::geolocation::{GeoLocator, GeolocationConfig};

fn lookup_example(locator: &GeoLocator) -> modo::Result<()> {
    let ip: IpAddr = "81.2.69.142".parse().unwrap();
    let location = locator.lookup(ip)?;

    println!("country: {:?}", location.country_code);
    println!("city:    {:?}", location.city);
    println!("tz:      {:?}", location.timezone);
    Ok(())
}
```

`lookup` returns a `Location` with all fields set to `None` for IPs not found
in the database (private ranges, loopback addresses, etc.).

### Middleware integration

`GeoLayer` resolves the location once per request and stores it in request
extensions. `ClientIpLayer` must run before `GeoLayer` so that `ClientIp` is
available when the lookup fires.

```rust,ignore
use modo::ip::ClientIpLayer;
use modo::geolocation::{GeoLayer, GeoLocator, GeolocationConfig};
use axum::Router;

fn build_router(locator: GeoLocator) -> Router {
    Router::new()
        // routes ...
        .layer(GeoLayer::new(locator))
        .layer(ClientIpLayer::new())
}
```

Axum applies `.layer()` calls in bottom-up order, so `ClientIpLayer` is listed
last to ensure it runs first. If `ClientIp` is absent from extensions, `GeoLayer`
passes the request through unchanged.

`GeoLayer` is also re-exported from the flat middleware index as
`modo::middlewares::Geo`:

```rust,ignore
use modo::middlewares as mw;

let app = Router::new()
    // routes ...
    .layer(mw::Geo::new(locator))
    .layer(mw::ClientIp::new());
```

### Extracting Location in a handler

```rust,ignore
use modo::geolocation::Location;

async fn handler(location: Location) -> String {
    match location.country_code {
        Some(code) => format!("Hello from {code}"),
        None => "Hello, unknown visitor".to_string(),
    }
}
```

`Location` implements `FromRequestParts` with `Infallible` rejection, so the
extraction always succeeds. When `GeoLayer` has not run or the IP was not in
the database, all fields are `None`.

## Location fields

| Field          | Type             | Example                 |
| -------------- | ---------------- | ----------------------- |
| `country_code` | `Option<String>` | `"US"`                  |
| `country_name` | `Option<String>` | `"United States"`       |
| `region`       | `Option<String>` | `"California"`          |
| `city`         | `Option<String>` | `"San Francisco"`       |
| `latitude`     | `Option<f64>`    | `37.7749`               |
| `longitude`    | `Option<f64>`    | `-122.4194`             |
| `timezone`     | `Option<String>` | `"America/Los_Angeles"` |