# AGENTS.md — lat-long
> Context file for AI coding agents. Describes architecture, conventions, and
> workflows relevant to this codebase.
## Project Overview
`lat-long` is a focused Rust library (`no_std`-compatible) providing strongly-typed
geographic coordinate primitives: `Latitude`, `Longitude`, and `Coordinate`.
Values are validated on construction and support both decimal-degrees and
degrees–minutes–seconds display/parsing.
- **Crate name:** `lat-long` (lib target: `lat_long`)
- **Version:** 0.1.1 — published to [crates.io](https://crates.io/crates/lat-long)
- **Edition:** 2024
- **MSRV:** Rust 1.90
- **License:** MIT OR Apache-2.0
---
## Source Layout
```
src/
lib.rs # Crate root; re-exports public types; defines the `Angle` trait
lat.rs # `Latitude` type, constants (NORTH_POLE, EQUATOR, …), `lat!` macro
long.rs # `Longitude` type, constants (PRIME_MERIDIAN, …), `lon!` macro
coord.rs # `Coordinate` (Latitude + Longitude pair)
alt.rs # `Altitude` and `Coordinate3d` — gated on feature `3d`
fmt.rs # `FormatOptions`, `FormatKind`, `Formatter` trait, formatting logic
parse.rs # `parse_str`, `Parsed` enum, `FromStr` impls
error.rs # `Error` enum (all validation & parse errors)
inner.rs # Private helpers: DMS ↔ decimal-degree conversions, `ZERO` const
tests/
latitude_tests.rs
longitude_tests.rs
parse_tests.rs
```
---
## Core Abstraction: the `Angle` Trait
`Angle` (defined in `src/lib.rs`) is the shared interface for `Latitude` and
`Longitude`. Any implementation must be:
- `Clone + Copy + Debug + Default + Display + PartialEq + Eq + PartialOrd + Ord + Hash`
- Convertible from/into `OrderedFloat<f64>` (provides NaN-free float comparison)
- Constructable via `Angle::new(degrees: i32, minutes: u32, seconds: f32) -> Result<Self, Error>`
Trait-provided methods include: `degrees()`, `minutes()`, `seconds()`,
`is_zero()`, `is_nonzero_positive()`, `is_nonzero_negative()`, `checked_abs()`,
`saturating_abs()`, `overflowing_abs()`, `wrapping_abs()`, `strict_abs()`,
`unchecked_abs()`.
**Key invariant:** sign lives only in `degrees`; `minutes` and `seconds` are
always non-negative.
---
## Feature Flags
| `std` | `alloc` + std lib, `serde/std`, `serde_json/std`, `3d`, `urn` | ✅ |
| `alloc` | `alloc` + `core` only (`no_std` mode) | via `std` |
| `serde` | `Serialize`/`Deserialize` for all coordinate types | no |
| `geojson` | `From<Coordinate>` for `serde_json::Value` (GeoJSON spec) | no |
| `urn` | `From<Coordinate>` for `url::Url` with `geo:` scheme (RFC 5870) | no |
| `3d` | `Altitude` + `Coordinate3d` types via `uom` | no |
Gate feature-specific code with `#[cfg(feature = "...")]`. The `3d` feature
name uses a numeric prefix — always quote it in attribute syntax:
`#[cfg(feature = "3d")]`.
---
## Key Dependencies
| `ordered-float` | `OrderedFloat<f64>` — NaN-free float with `Ord` + `Hash` |
| `serde` | Serialization derives (optional) |
| `serde_json` | JSON output for `geojson` feature (optional) |
| `uom` | Type-safe units-of-measure for altitude (optional) |
| `url` | `geo:` URN construction (optional) |
| `document-features` | Auto-generates feature-flag docs in `lib.rs` |
---
## Display & Formatting
The `fmt` module provides `FormatOptions` and the `Formatter` trait. Four
format kinds are supported:
| `Decimal` | `48.858222` | `-48.858222` |
| `DmsSigned` | `48° 51′ 29.600000″` | `-48° 51′ 29.600000″` |
| `DmsLabeled` | `48° 51′ 29.600000″ N` | `48° 51′ 29.600000″ S` |
| `DmsBare` | `+048:51:29.600000` | `-048:51:29.600000` |
- `Display` on a type → `Decimal` (default)
- `Display` with alternate flag `{:#}` → `DmsSigned`
- `DmsBare` is a machine-parseable format; no whitespace around the comma in coordinate pairs.
---
## Error Handling
All errors are variants of `error::Error` and implement `Display` + `std::error::Error`.
Construction methods (`Angle::new`, `TryFrom<f64>`) return `Result<Self, Error>`.
Never use `.unwrap()` in library code. Use `.expect("reason")` only in test helpers
or doc examples that are guaranteed to succeed.
---
## Parsing
`parse::parse_str(s: &str) -> Result<Parsed, Error>` accepts all four format
kinds and returns a `Parsed` enum:
```rust
pub enum Parsed {
Angle(Value), // Value::Latitude, Value::Longitude, or Value::Unknown
Coordinate(Coordinate),
}
```
`FromStr` is implemented for `Latitude`, `Longitude`, and `Coordinate` by
delegating to `parse_str`.
---
## Build & Workflow Commands
All standard workflows are driven through `make` (see `Makefile`).
| Format | `make format` | Runs `cargo fmt` |
| Lint | `make clippy` | Runs `cargo clippy --workspace` |
| Build matrix | `make build` | Default, all-features, no-default-features × debug+release |
| Test matrix | `make test` | Runs across default, all-features, no-default-features, and per-feature combos |
| Coverage | `make coverage` | Runs `cargo tarpaulin` (see `.tarpaulin.toml`) |
| Docs | `make docs` | `cargo doc --all-features --no-deps` |
| Publish check | `make publish` | Dry-run; runs full lint+test+docs first |
**Full test matrix** (run by `make test`):
```
cargo test --workspace
cargo test --workspace --all-features
cargo test --workspace --no-default-features
cargo test --workspace --no-default-features --features "urn"
cargo test --workspace --no-default-features --features "serde"
cargo test --workspace --no-default-features --features "geojson"
cargo test --workspace --no-default-features --features "urn,serde,geojson"
```
Always run `make test` before committing — never just `cargo test`.
---
## Testing Conventions
- Integration tests live in `tests/`; unit tests (if any) live in `#[cfg(test)]`
modules inside `src/`.
- Test file naming: `<type>_tests.rs` (e.g., `latitude_tests.rs`).
- **Test behavior, not implementation.** Every test should answer: _"what user-visible
behavior breaks if this test fails?"_
- Prefer testing:
- Valid construction succeeds and round-trips correctly.
- Out-of-range / invalid inputs return the expected `Error` variant.
- `Display` output matches expected string exactly.
- `FromStr` parses back to the original value.
- Feature-gated code (serde, geojson, urn) is tested under the appropriate feature flag.
- Avoid tests that only assert a function was called or a mock was invoked.
---
## Code Style Guidelines
- Follow the existing module layout; one primary type per module.
- Use `#[must_use]` on pure query methods (see `Latitude::is_northern`, etc.).
- `inner` module is `pub(crate)` only — never expose it publicly.
- All public items require doc comments with at least one `# Examples` section.
- Macro naming: `lat!` / `lon!` — lowercase, matching the module name.
- Do **not** use `unwrap()` in library code; use typed errors + `?`.
- `no_std` compatibility: avoid importing from `std::` directly in feature-gated
code; prefer `core::` and `alloc::` where possible.
- Format strings use Unicode typographic characters: `°` `′` `″` (not `'` `"`).
---
## Coverage
Coverage is configured in `.tarpaulin.toml` with named profiles:
| `default_release`| Default features, release mode |
| `all_features` | All features, debug mode |
| `no_std` | `--no-default-features --features alloc` |
| `default_serde` | `--features serde` |
| `default_url` | `--features url` |
| `geojson` | `--features geojson` |
Output formats: HTML and XML (see `[report]` section).
---
## Publishing Checklist
1. Bump `version` in `Cargo.toml`.
2. Run `make all` (format → clippy → test → docs).
3. Run `make publish` (dry-run).
4. Tag the commit and push.
5. Run `cargo publish`.