libfmp 0.4.0

Async, typed Rust client for the Financial Modeling Prep API
Documentation
use serde::{Serialize, de::DeserializeOwned};
use serde_json::{Map, Value, json};

use libfmp::responses::commitment_of_traders::{CotAnalysis, CotReport, CotReportListing};

const REPORT: &[u8] = include_bytes!("fixtures/cot_report.json");
const ANALYSIS: &[u8] = include_bytes!("fixtures/cot_analysis.json");
const REPORT_LIST: &[u8] = include_bytes!("fixtures/cot_report_list.json");

#[test]
fn all_documented_cot_fixtures_decode_and_round_trip_exactly() {
    assert_exact::<CotReport>(REPORT);
    assert_exact::<CotAnalysis>(ANALYSIS);
    assert_exact::<CotReportListing>(REPORT_LIST);

    let report = rows::<CotReport>(REPORT).remove(0);
    assert_eq!(report.symbol.as_str(), "VX");
    assert_eq!(report.date.to_string(), "2024-02-27 00:00:00");
    assert_eq!(report.sector.as_str(), "INDICES");
    assert_eq!(report.change_in_noncomm_spread_all, 9_257);
    assert_eq!(report.traders_noncomm_spread_old, 101);
    assert_eq!(report.contract_units, "($1000 X INDEX)");

    let analysis = rows::<CotAnalysis>(ANALYSIS).remove(0);
    assert_eq!(
        analysis.exchange,
        "PALLADIUM - NEW YORK MERCANTILE EXCHANGE"
    );
    assert_eq!(analysis.net_position, -12_315);
    assert!(analysis.reversal_trend);
}

#[test]
fn report_has_exactly_the_documented_128_unique_keys_and_wire_hazards() {
    let source: Value = serde_json::from_slice(REPORT).unwrap();
    let source_row = source[0].as_object().unwrap();
    assert_eq!(source_row.len(), 128);

    let report = rows::<CotReport>(REPORT).remove(0);
    let encoded = serde_json::to_value(report).unwrap();
    let encoded_row = encoded.as_object().unwrap();
    assert_eq!(encoded_row.len(), 128);
    assert_eq!(
        encoded_row.keys().collect::<Vec<_>>(),
        source_row.keys().collect::<Vec<_>>()
    );
    for key in [
        "changeInNoncommSpeadAll",
        "tradersNoncommSpeadOl",
        "pctOfOpenInterestOl",
        "concGrossLe4TdrLongOl",
    ] {
        assert!(
            encoded_row.contains_key(key),
            "missing exact wire key {key}"
        );
    }
    for corrected_but_wrong_wire_key in [
        "changeInNoncommSpreadAll",
        "tradersNoncommSpreadOld",
        "pctOfOpenInterestOld",
        "concGrossLe4TdrLongOld",
    ] {
        assert!(!encoded_row.contains_key(corrected_but_wrong_wire_key));
    }
}

#[test]
fn report_preserves_integer_and_decimal_number_spellings() {
    let report = rows::<CotReport>(REPORT).remove(0);
    let encoded = serde_json::to_string(&report).unwrap();
    assert!(encoded.contains(r#""pctOfOpenInterestAll":100"#));
    assert!(encoded.contains(r#""pctOfOiNoncommLongAll":20.6"#));
    assert!(encoded.contains(r#""pctOfOiNoncommSpreadAll":27"#));
    assert!(encoded.contains(r#""concGrossLe8TdrShortAll":29"#));
    assert!(!encoded.contains(r#""pctOfOpenInterestAll":100.0"#));
    assert!(!encoded.contains(r#""pctOfOiNoncommSpreadAll":27.0"#));
}

#[test]
fn every_cot_field_is_required_non_null_and_unknown_fields_are_tolerated() {
    assert_required_contract::<CotReport>(REPORT, 128);
    assert_required_contract::<CotAnalysis>(ANALYSIS, 16);
    assert_required_contract::<CotReportListing>(REPORT_LIST, 2);
}

#[test]
fn every_cot_contract_requires_a_bare_array_root() {
    assert!(
        serde_json::from_str::<Vec<CotReport>>("[]")
            .unwrap()
            .is_empty()
    );
    assert!(
        serde_json::from_str::<Vec<CotAnalysis>>("[]")
            .unwrap()
            .is_empty()
    );
    assert!(
        serde_json::from_str::<Vec<CotReportListing>>("[]")
            .unwrap()
            .is_empty()
    );
    assert!(serde_json::from_str::<Vec<CotReport>>("{}").is_err());
    assert!(serde_json::from_str::<Vec<CotAnalysis>>("{}").is_err());
    assert!(serde_json::from_str::<Vec<CotReportListing>>("{}").is_err());
}

fn rows<T: DeserializeOwned>(fixture: &[u8]) -> Vec<T> {
    serde_json::from_slice(fixture).unwrap()
}

fn assert_exact<T>(fixture: &[u8])
where
    T: DeserializeOwned + Serialize,
{
    let source: Value = serde_json::from_slice(fixture).unwrap();
    assert_eq!(serde_json::to_value(rows::<T>(fixture)).unwrap(), source);
}

fn assert_required_contract<T>(fixture: &[u8], field_count: usize)
where
    T: DeserializeOwned,
{
    let source: Value = serde_json::from_slice(fixture).unwrap();
    let row = source[0].as_object().unwrap();
    assert_eq!(row.len(), field_count);

    for field in row.keys() {
        let mut missing = row.clone();
        missing.remove(field);
        assert!(
            serde_json::from_value::<T>(Value::Object(missing)).is_err(),
            "field {field} must be present"
        );

        let mut null = row.clone();
        null.insert(field.clone(), Value::Null);
        assert!(
            serde_json::from_value::<T>(Value::Object(null)).is_err(),
            "field {field} must not accept null"
        );
    }

    let mut future: Map<String, Value> = row.clone();
    future.insert("futureField".into(), json!({"nested": true}));
    assert!(serde_json::from_value::<T>(Value::Object(future)).is_ok());
}