libfmp 0.4.0

Async, typed Rust client for the Financial Modeling Prep API
Documentation
use std::str::FromStr;

use libfmp::{
    query::FiscalPeriod,
    responses::statements::RevenueSegmentation,
    types::{CalendarYear, CurrencyCode, Date, Ticker},
};
use serde::de::DeserializeOwned;

const PRODUCT: &[u8] = include_bytes!("fixtures/revenue_product_segmentation.json");
const GEOGRAPHIC: &[u8] = include_bytes!("fixtures/revenue_geographic_segmentation.json");

#[test]
fn product_fixture_decodes_exact_envelope_and_arbitrary_product_keys() {
    let value: serde_json::Value = serde_json::from_slice(PRODUCT).unwrap();
    assert_eq!(value[0].as_object().unwrap().len(), 6);

    let rows: Vec<RevenueSegmentation> = serde_json::from_value(value).unwrap();
    let row = &rows[0];
    assert_eq!(row.symbol, Ticker::new("AAPL").unwrap());
    assert_eq!(row.fiscal_year, CalendarYear(2025));
    assert_eq!(row.period, FiscalPeriod::FullYear);
    assert_eq!(row.reported_currency, CurrencyCode::new("USD").unwrap());
    assert_eq!(row.date, Date::from_str("2025-09-27").unwrap());
    assert_eq!(row.data.len(), 5);
    assert_eq!(row.data["Mac"], serde_json::json!(33_708_000_000_u64));
    assert_eq!(
        row.data["Wearables, Home and Accessories"],
        serde_json::json!(35_686_000_000_u64)
    );
    assert_eq!(row.data["iPhone"], serde_json::json!(209_586_000_000_u64));
}

#[test]
fn geographic_fixture_decodes_exact_shared_shape_and_spaced_keys() {
    let value: serde_json::Value = serde_json::from_slice(GEOGRAPHIC).unwrap();
    assert_eq!(value[0].as_object().unwrap().len(), 6);

    let rows: Vec<RevenueSegmentation> = serde_json::from_value(value).unwrap();
    assert_eq!(rows[0].data.len(), 5);
    assert_eq!(
        rows[0].data["Greater China Segment"],
        serde_json::json!(64_377_000_000_u64)
    );
    assert_eq!(
        rows[0].data["Rest of Asia Pacific Segment"],
        serde_json::json!(33_696_000_000_u64)
    );
}

#[test]
fn dynamic_data_preserves_open_keys_values_and_large_integer_tokens() {
    let mut value: serde_json::Value = serde_json::from_slice(PRODUCT).unwrap();
    value[0]["data"]["Consumer / Pro, North America (Rest)"] =
        serde_json::from_str("9007199254740993").unwrap();
    value[0]["data"]["Future Segment: beta + services"] =
        serde_json::json!({ "reported": true, "adjustments": [null, -7] });

    let rows: Vec<RevenueSegmentation> = serde_json::from_value(value).unwrap();
    assert_eq!(
        rows[0].data["Consumer / Pro, North America (Rest)"].to_string(),
        "9007199254740993"
    );
    assert_eq!(
        rows[0].data["Future Segment: beta + services"]["adjustments"][1],
        serde_json::json!(-7)
    );

    let encoded = serde_json::to_string(&rows).unwrap();
    assert!(encoded.contains("9007199254740993"));
    assert!(!encoded.contains("9007199254740992"));
}

#[test]
fn every_envelope_field_is_required_non_null_and_exactly_typed() {
    for fixture in [PRODUCT, GEOGRAPHIC] {
        assert_every_field_required_and_non_null::<RevenueSegmentation>(fixture);
    }

    let mut string_year: serde_json::Value = serde_json::from_slice(PRODUCT).unwrap();
    string_year[0]["fiscalYear"] = serde_json::json!("2025");
    assert!(serde_json::from_value::<Vec<RevenueSegmentation>>(string_year).is_err());

    let mut retrieval_period: serde_json::Value = serde_json::from_slice(PRODUCT).unwrap();
    retrieval_period[0]["period"] = serde_json::json!("annual");
    assert!(serde_json::from_value::<Vec<RevenueSegmentation>>(retrieval_period).is_err());

    let mut array_data: serde_json::Value = serde_json::from_slice(PRODUCT).unwrap();
    array_data[0]["data"] = serde_json::json!([]);
    assert!(serde_json::from_value::<Vec<RevenueSegmentation>>(array_data).is_err());
}

fn assert_every_field_required_and_non_null<T: DeserializeOwned>(fixture: &[u8]) {
    let source: serde_json::Value = serde_json::from_slice(fixture).unwrap();
    let keys = source[0]
        .as_object()
        .unwrap()
        .keys()
        .cloned()
        .collect::<Vec<_>>();

    for key in keys {
        let mut missing = source.clone();
        missing[0].as_object_mut().unwrap().remove(&key);
        assert!(
            serde_json::from_value::<Vec<T>>(missing).is_err(),
            "accepted missing field {key}"
        );

        let mut null = source.clone();
        null[0][&key] = serde_json::Value::Null;
        assert!(
            serde_json::from_value::<Vec<T>>(null).is_err(),
            "accepted null field {key}"
        );
    }
}

#[test]
fn both_endpoints_are_bare_arrays_preserving_empty_and_multiple_rows() {
    assert!(
        serde_json::from_slice::<Vec<RevenueSegmentation>>(b"[]")
            .unwrap()
            .is_empty()
    );
    for fixture in [PRODUCT, GEOGRAPHIC] {
        let mut value: serde_json::Value = serde_json::from_slice(fixture).unwrap();
        let duplicate = value[0].clone();
        value.as_array_mut().unwrap().push(duplicate);
        assert_eq!(
            serde_json::from_value::<Vec<RevenueSegmentation>>(value)
                .unwrap()
                .len(),
            2
        );
    }

    assert!(
        serde_json::from_value::<Vec<RevenueSegmentation>>(serde_json::json!({ "segments": [] }))
            .is_err()
    );
}