pleiades-backend 0.3.0

Backend traits, request/result contracts, capability metadata, policy summaries, and routing helpers for pleiades ephemeris backends.
Documentation
use crate::*;

#[test]
fn ephemeris_result_has_a_compact_display() {
    let instant = Instant::new(JulianDay::from_days(2_451_545.0), TimeScale::Tt);
    let mut result = EphemerisResult::new(
        BackendId::new("toy"),
        CelestialBody::Sun,
        instant,
        CoordinateFrame::Ecliptic,
        ZodiacMode::Tropical,
        Apparentness::Mean,
    );
    result.ecliptic = Some(EclipticCoordinates::new(
        Longitude::from_degrees(12.5),
        Latitude::from_degrees(-3.25),
        Some(1.234),
    ));
    result.equatorial = Some(EquatorialCoordinates::new(
        Angle::from_degrees(98.0),
        Latitude::from_degrees(0.5),
        None,
    ));
    result.motion = Some(Motion::new(Some(0.1), Some(-0.2), Some(0.003)));
    result.quality = QualityAnnotation::Exact;

    assert_eq!(result.to_string(), result.summary_line());
    assert_eq!(
            result.summary_line(),
            "backend=toy; body=Sun; instant=JD 2451545 TT; frame=Ecliptic; zodiac=Tropical; apparent=Mean; quality=Exact; ecliptic=longitude=12.5°, latitude=-3.25°, distance=1.234 AU; equatorial=right_ascension=98°, declination=0.5°, distance=n/a; motion=longitude_speed=0.1 deg/day, latitude_speed=-0.2 deg/day, distance_speed=0.003 AU/day"
        );
    assert!(result.summary_line().contains("backend=toy"));
    assert!(result.summary_line().contains("quality=Exact"));
    assert!(result.summary_line().contains("ecliptic=longitude=12.5°"));
}

#[cfg(feature = "serde")]
#[test]
fn serde_roundtrip_preserves_requests_and_results() {
    let request = EphemerisRequest {
        body: CelestialBody::Custom(CustomBodyId::new("asteroid", "433-Eros")),
        instant: Instant::new(JulianDay::from_days(2451545.0), TimeScale::Tt),
        observer: Some(ObserverLocation::new(
            Latitude::from_degrees(51.5),
            Longitude::from_degrees(-0.1),
            Some(45.0),
        )),
        frame: CoordinateFrame::Equatorial,
        zodiac_mode: ZodiacMode::Sidereal {
            ayanamsa: Ayanamsa::Lahiri,
        },
        apparent: Apparentness::Mean,
    };
    let request_roundtrip: EphemerisRequest =
        serde_json::from_value(serde_json::to_value(&request).expect("request should serialize"))
            .expect("request should deserialize");
    assert_eq!(request_roundtrip, request);

    let mut result = EphemerisResult::new(
        BackendId::new("toy"),
        CelestialBody::Moon,
        Instant::new(JulianDay::from_days(2451545.0), TimeScale::Tt),
        CoordinateFrame::Ecliptic,
        ZodiacMode::Tropical,
        Apparentness::Apparent,
    );
    result.quality = QualityAnnotation::Interpolated;
    result.ecliptic = Some(EclipticCoordinates::new(
        Longitude::from_degrees(123.0),
        Latitude::from_degrees(2.5),
        Some(1.0),
    ));
    result.motion = Some(Motion::new(Some(0.12), Some(-0.01), None));

    let result_roundtrip: EphemerisResult =
        serde_json::from_value(serde_json::to_value(&result).expect("result should serialize"))
            .expect("result should deserialize");
    assert_eq!(result_roundtrip, result);
}

#[test]
fn ephemeris_result_validation_rejects_invalid_coordinate_and_motion_samples() {
    let mut result = EphemerisResult::new(
        BackendId::new("toy"),
        CelestialBody::Moon,
        Instant::new(JulianDay::from_days(2_451_545.0), TimeScale::Tt),
        CoordinateFrame::Ecliptic,
        ZodiacMode::Tropical,
        Apparentness::Mean,
    );
    result.ecliptic = Some(EclipticCoordinates::new(
        Longitude::from_degrees(12.5),
        Latitude::from_degrees(2.5),
        Some(1.0),
    ));
    result.equatorial = Some(EquatorialCoordinates::new(
        Angle::from_degrees(f64::NAN),
        Latitude::from_degrees(1.0),
        Some(1.0),
    ));
    result.motion = Some(Motion::new(Some(f64::INFINITY), None, None));

    let error = result
        .validate()
        .expect_err("invalid equatorial coordinates should fail validation");
    assert!(matches!(
        error,
        EphemerisResultValidationError::InvalidEquatorial(
            CoordinateValidationError::NonFiniteValue {
                coordinate: "equatorial",
                field: "right_ascension",
                value,
            }
        ) if value.is_nan()
    ));
    assert!(error
            .to_string()
            .contains("backend result equatorial is invalid: equatorial coordinate field `right_ascension` must be finite"));

    result.equatorial = None;
    let error = result
        .validated_summary_line()
        .expect_err("invalid motion should fail validation");
    assert!(matches!(
        error,
        EphemerisResultValidationError::InvalidMotion(MotionValidationError::NonFiniteSpeed {
            field: "longitude_deg_per_day",
            value,
        }) if value.is_infinite()
    ));
    assert!(error.to_string().contains(
        "backend result motion is invalid: motion field `longitude_deg_per_day` must be finite"
    ));
}