libfmp 0.3.0

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

use libfmp::{
    endpoints::{metadata::GeographicAvailability, transcripts::earnings_transcript_list},
    query::FiscalPeriod,
    responses::transcripts::{
        EarningsTranscript, EarningsTranscriptAvailability, EarningsTranscriptDate,
        LatestEarningsTranscript,
    },
    types::{CalendarQuarter, CalendarYear, Date, Ticker},
};
use serde::de::DeserializeOwned;

const LATEST: &[u8] = include_bytes!("fixtures/latest_earnings_transcripts.json");
const TRANSCRIPT: &[u8] = include_bytes!("fixtures/earnings_transcript.json");
const DATES: &[u8] = include_bytes!("fixtures/earnings_transcript_dates.json");
const AVAILABILITY: &[u8] = include_bytes!("fixtures/directory_earnings_transcript_list.json");

const EXACT_CONTENT: &str = "Operator: Good day, everyone. Welcome to the Apple Incorporated Third Quarter Fiscal Year 2020 Earnings Conference Call. Today's call is being recorded. At this time, for opening remarks and introductions, I would like to turn things over to Mr. Tejas Gala, Senior Manager, Corporate Finance and Investor Relations. Please go ahead, sir.\nTejas Gala: Thank you. Good afternoon and thank you for joining us. Speaking first today is Apple's CEO, Tim Cook; and he'll be followed by CFO, Luca Maestri. Aft...";

#[test]
fn exact_latest_fixture_decodes_four_required_metadata_fields() {
    assert_field_count(LATEST, 4);
    let rows: Vec<LatestEarningsTranscript> = serde_json::from_slice(LATEST).unwrap();
    assert_eq!(
        rows,
        [LatestEarningsTranscript {
            symbol: Ticker::new("VLO").unwrap(),
            period: FiscalPeriod::Q2,
            fiscal_year: CalendarYear(2026),
            date: Date::from_str("2026-07-30").unwrap(),
        }]
    );
}

#[test]
fn exact_transcript_fixture_retains_the_complete_documented_content() {
    assert_field_count(TRANSCRIPT, 5);
    let rows: Vec<EarningsTranscript> = serde_json::from_slice(TRANSCRIPT).unwrap();
    assert_eq!(
        rows,
        [EarningsTranscript {
            symbol: Ticker::new("AAPL").unwrap(),
            period: FiscalPeriod::Q3,
            year: CalendarYear(2020),
            date: Date::from_str("2020-07-30").unwrap(),
            content: EXACT_CONTENT.to_owned(),
        }]
    );
    assert!(rows[0].content.contains("\nTejas Gala:"));
    assert!(rows[0].content.ends_with("Aft..."));
}

#[test]
fn exact_dates_fixture_uses_numeric_response_quarter_and_year() {
    assert_field_count(DATES, 3);
    let rows: Vec<EarningsTranscriptDate> = serde_json::from_slice(DATES).unwrap();
    assert_eq!(
        rows,
        [EarningsTranscriptDate {
            quarter: CalendarQuarter::new(2).unwrap(),
            fiscal_year: CalendarYear(2026),
            date: Date::from_str("2026-04-30").unwrap(),
        }]
    );

    for invalid in [serde_json::json!("2"), serde_json::json!(2.0)] {
        let mut value: serde_json::Value = serde_json::from_slice(DATES).unwrap();
        value[0]["quarter"] = invalid;
        assert!(serde_json::from_value::<Vec<EarningsTranscriptDate>>(value).is_err());
    }

    for invalid in [serde_json::json!("2026"), serde_json::json!(2026.0)] {
        for field in ["fiscalYear", "year"] {
            let fixture = if field == "year" { TRANSCRIPT } else { LATEST };
            let mut value: serde_json::Value = serde_json::from_slice(fixture).unwrap();
            value[0][field] = invalid.clone();
            if field == "year" {
                assert!(serde_json::from_value::<Vec<EarningsTranscript>>(value).is_err());
            } else {
                assert!(serde_json::from_value::<Vec<LatestEarningsTranscript>>(value).is_err());
            }
        }
    }
}

#[test]
fn transcript_content_is_unbounded_and_preserved_exactly() {
    let chunk = "Operator: café 中文 📈.\nQuoted: \"guidance\"; path=C:\\reports\\Q4; tab=\tend.\n";
    let content = chunk.repeat(40_000);
    assert!(content.len() > 2 * 1024 * 1024);
    let value = serde_json::json!([{
        "symbol": "BIG",
        "period": "Q4",
        "year": 4294967295_u32,
        "date": "2026-07-30",
        "content": content,
    }]);
    let wire = serde_json::to_vec(&value).unwrap();
    assert!(wire.windows(2).any(|window| window == b"\\n"));
    assert!(wire.windows(2).any(|window| window == b"\\\""));
    assert!(wire.windows(2).any(|window| window == b"\\\\"));

    let rows: Vec<EarningsTranscript> = serde_json::from_slice(&wire).unwrap();
    assert_eq!(rows[0].year, CalendarYear(u32::MAX));
    assert_eq!(rows[0].content.len(), content.len());
    assert_eq!(rows[0].content, content);
}

#[test]
fn every_documented_field_is_required_non_null_and_unknowns_are_accepted() {
    assert_contract::<LatestEarningsTranscript>(LATEST);
    assert_contract::<EarningsTranscript>(TRANSCRIPT);
    assert_contract::<EarningsTranscriptDate>(DATES);
}

fn assert_contract<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 {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 {key}"
        );
    }

    let mut forward = source;
    forward[0]["futureProviderField"] = serde_json::json!({ "nested": [1, true, null] });
    let duplicate = forward[0].clone();
    forward.as_array_mut().unwrap().push(duplicate);
    assert_eq!(serde_json::from_value::<Vec<T>>(forward).unwrap().len(), 2);
}

fn assert_field_count(fixture: &[u8], expected: usize) {
    let value: serde_json::Value = serde_json::from_slice(fixture).unwrap();
    assert_eq!(value[0].as_object().unwrap().len(), expected);
}

#[test]
fn all_transcript_rows_are_bare_arrays_preserving_empty_shapes() {
    assert!(
        serde_json::from_slice::<Vec<LatestEarningsTranscript>>(b"[]")
            .unwrap()
            .is_empty()
    );
    assert!(
        serde_json::from_slice::<Vec<EarningsTranscript>>(b"[]")
            .unwrap()
            .is_empty()
    );
    assert!(
        serde_json::from_slice::<Vec<EarningsTranscriptDate>>(b"[]")
            .unwrap()
            .is_empty()
    );
    assert!(
        serde_json::from_value::<Vec<EarningsTranscript>>(serde_json::json!({
            "transcripts": []
        }))
        .is_err()
    );
}

#[test]
fn transcript_module_reexports_the_existing_us_only_availability_contract() {
    let endpoint = earnings_transcript_list();
    assert_eq!(endpoint.id(), "earnings-transcript-list");
    assert_eq!(
        endpoint.metadata().geography(),
        GeographicAvailability::UsOnly
    );

    let rows: Vec<EarningsTranscriptAvailability> = serde_json::from_slice(AVAILABILITY).unwrap();
    assert_eq!(rows[0].symbol.as_str(), "INBS");
    assert_eq!(rows[0].no_of_transcripts.as_str(), "6");
}