aerocontext-awc 0.2.0

aviationweather.gov Data API adapter for aerocontext-core
Documentation
use super::*;

#[test]
fn metar_decodes_field_names_from_spec() {
    let body = r#"[{
        "icaoId": "KORD",
        "rawOb": "KORD 041951Z 27012KT 10SM FEW250 28/14 A3002",
        "obsTime": 1717530660,
        "temp": 28.0,
        "altim": 1016.6
    }]"#;
    let products = metars(body).unwrap();
    assert_eq!(products.len(), 1);
    assert_eq!(products[0].kind, ProductKind::Metar);
    assert_eq!(products[0].location.as_deref(), Some("KORD"));
    assert!(products[0].raw_text.starts_with("KORD 041951Z"));
    assert_eq!(
        products[0].issued_at.map(|t| t.timestamp()),
        Some(1717530660)
    );
}

#[test]
fn metar_without_raw_text_is_dropped_not_fatal() {
    let body = r#"[{"icaoId": "KORD"}, {"icaoId": "KMDW", "rawOb": "KMDW ..."}]"#;
    let products = metars(body).unwrap();
    assert_eq!(products.len(), 1);
    assert_eq!(products[0].location.as_deref(), Some("KMDW"));
}

#[test]
fn taf_uses_raw_taf_not_raw_ob() {
    let body = r#"[{
        "icaoId": "KORD",
        "rawTAF": "KORD 041730Z 0418/0524 27010KT P6SM SCT250",
        "issueTime": "2026-06-04 17:30:00"
    }]"#;
    let products = tafs(body).unwrap();
    assert_eq!(products.len(), 1);
    assert_eq!(products[0].kind, ProductKind::Taf);
    assert!(products[0].raw_text.starts_with("KORD 041730Z"));
    assert!(products[0].issued_at.is_some());
}

#[test]
fn pirep_decodes_raw_ob() {
    let body = r#"[{
        "rawOb": "ORD UA /OV ORD/TM 1955/FL080/TP B738/TB LGT",
        "obsTime": 1717531000
    }]"#;
    let products = pireps(body).unwrap();
    assert_eq!(products.len(), 1);
    assert_eq!(products[0].kind, ProductKind::Pirep);
    assert!(products[0].location.is_none());
}

#[test]
fn pirep_fractional_obs_time_does_not_fail_the_batch() {
    // The spec types PIREP obsTime as `number`, so the endpoint may emit
    // a fractional epoch; one float must not abort the whole array.
    let body = r#"[
        {"rawOb": "FLOAT .0", "obsTime": 1699379880.0},
        {"rawOb": "FLOAT .5", "obsTime": 1699379880.5},
        {"rawOb": "INTEGER", "obsTime": 1699379880}
    ]"#;
    let products = pireps(body).unwrap();
    assert_eq!(products.len(), 3);
    for product in &products {
        assert_eq!(
            product.issued_at.map(|t| t.timestamp()),
            Some(1699379880),
            "{} keeps its (truncated) timestamp",
            product.raw_text
        );
    }
}

#[test]
fn airsigmet_carries_polygon_for_clipping() {
    let body = r#"[{
        "icaoId": "KKCI",
        "rawAirSigmet": "WSUS31 KKCI 042000 ...",
        "creationTime": "2026-06-04T19:48:24.974Z",
        "coords": [{"lat": 40.0, "lon": -90.0}, {"lat": 42.0, "lon": -88.0}]
    }]"#;
    let entries = air_sigmets(body).unwrap();
    assert_eq!(entries.len(), 1);
    assert_eq!(entries[0].0.kind, ProductKind::Sigmet);
    assert_eq!(entries[0].1.len(), 2);
    assert!(entries[0].0.issued_at.is_some());
}

#[test]
fn clip_keeps_intersecting_and_geometryless_drops_outside() {
    let inside = (
        Product::new(ProductKind::Sigmet, "INSIDE"),
        vec![
            GeoPoint {
                lat: 40.0,
                lon: -90.0,
            },
            GeoPoint {
                lat: 42.0,
                lon: -88.0,
            },
        ],
    );
    let outside = (
        Product::new(ProductKind::Sigmet, "OUTSIDE"),
        vec![
            GeoPoint {
                lat: 10.0,
                lon: 50.0,
            },
            GeoPoint {
                lat: 12.0,
                lon: 52.0,
            },
        ],
    );
    let no_geometry = (Product::new(ProductKind::Sigmet, "NO-GEOMETRY"), vec![]);
    // Overlap on exactly one axis must still be clipped: these two pin
    // the longitude check and the latitude check independently, so a
    // single-axis regression cannot pass.
    let lat_overlap_only = (
        Product::new(ProductKind::Sigmet, "LAT-ONLY"),
        vec![
            GeoPoint {
                lat: 40.0,
                lon: 50.0,
            },
            GeoPoint {
                lat: 41.0,
                lon: 52.0,
            },
        ],
    );
    let lon_overlap_only = (
        Product::new(ProductKind::Sigmet, "LON-ONLY"),
        vec![
            GeoPoint {
                lat: 10.0,
                lon: -90.0,
            },
            GeoPoint {
                lat: 12.0,
                lon: -88.0,
            },
        ],
    );
    let kept = clip_to_bbox(
        vec![
            inside,
            outside,
            no_geometry,
            lat_overlap_only,
            lon_overlap_only,
        ],
        (
            GeoPoint {
                lat: 39.0,
                lon: -91.0,
            },
            GeoPoint {
                lat: 41.0,
                lon: -87.0,
            },
        ),
    );
    let texts: Vec<&str> = kept.iter().map(|p| p.raw_text.as_str()).collect();
    assert_eq!(texts, ["INSIDE", "NO-GEOMETRY"]);
}

#[test]
fn awc_time_formats_parse_best_effort() {
    assert!(parse_awc_time("2026-06-04T19:48:24.974Z").is_some());
    assert!(parse_awc_time("2026-06-04 19:48:24.974Z").is_some());
    assert!(parse_awc_time("2026-06-04 19:48:24").is_some());
    assert!(parse_awc_time("not a time").is_none());
}

#[test]
fn malformed_body_is_an_error_not_a_panic() {
    assert!(metars("{\"not\": \"an array\"}").is_err());
    assert!(metars("").is_err());
}