alpaca-data 0.10.2

High-performance Rust client for Alpaca Market Data API
Documentation
use alpaca_data::{Client, Error, options};
use futures_util::StreamExt;
use wiremock::matchers::{method, path, query_param, query_param_is_missing};
use wiremock::{Mock, MockServer, ResponseTemplate};

fn authed_client(base_url: String) -> Client {
    Client::builder()
        .api_key("key")
        .secret_key("secret")
        .base_url(base_url)
        .build()
        .expect("client should build")
}

#[tokio::test]
async fn malformed_snapshots_json_maps_to_deserialize_error() {
    let server = MockServer::start().await;

    Mock::given(method("GET"))
        .and(path("/v1beta1/options/snapshots"))
        .and(query_param("symbols", "AAPL260406C00180000"))
        .respond_with(ResponseTemplate::new(200).set_body_raw("not-json", "application/json"))
        .mount(&server)
        .await;

    let error = authed_client(server.uri())
        .options()
        .snapshots(options::SnapshotsRequest {
            symbols: vec!["AAPL260406C00180000".into()],
            ..Default::default()
        })
        .await
        .expect_err("request should fail");

    assert!(matches!(error, Error::Deserialize(_)));
}

#[tokio::test]
async fn malformed_condition_codes_json_maps_to_deserialize_error() {
    let server = MockServer::start().await;

    Mock::given(method("GET"))
        .and(path("/v1beta1/options/meta/conditions/trade"))
        .respond_with(ResponseTemplate::new(200).set_body_raw("not-json", "application/json"))
        .mount(&server)
        .await;

    let error = authed_client(server.uri())
        .options()
        .condition_codes(options::ConditionCodesRequest {
            ticktype: options::TickType::Trade,
        })
        .await
        .expect_err("request should fail");

    assert!(matches!(error, Error::Deserialize(_)));
}

#[tokio::test]
async fn snapshots_all_rejects_duplicate_symbols_across_pages() {
    let server = MockServer::start().await;

    Mock::given(method("GET"))
        .and(path("/v1beta1/options/snapshots"))
        .and(query_param(
            "symbols",
            "AAPL260406C00180000,AAPL260406C00185000",
        ))
        .and(query_param("limit", "1"))
        .and(query_param_is_missing("page_token"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "snapshots": {
                "AAPL260406C00180000": {
                    "latestQuote": { "bp": 73.95 }
                }
            },
            "next_page_token": "page-2"
        })))
        .mount(&server)
        .await;

    Mock::given(method("GET"))
        .and(path("/v1beta1/options/snapshots"))
        .and(query_param(
            "symbols",
            "AAPL260406C00180000,AAPL260406C00185000",
        ))
        .and(query_param("limit", "1"))
        .and(query_param("page_token", "page-2"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "snapshots": {
                "AAPL260406C00180000": {
                    "latestQuote": { "bp": 74.00 }
                }
            },
            "next_page_token": null
        })))
        .mount(&server)
        .await;

    let error = authed_client(server.uri())
        .options()
        .snapshots_all(options::SnapshotsRequest {
            symbols: vec!["AAPL260406C00180000".into(), "AAPL260406C00185000".into()],
            limit: Some(1),
            ..Default::default()
        })
        .await
        .expect_err("pagination should reject duplicate symbols");

    assert!(matches!(error, Error::Pagination(_)));
}

#[tokio::test]
async fn chain_stream_rejects_duplicate_symbols_across_pages() {
    let server = MockServer::start().await;

    Mock::given(method("GET"))
        .and(path("/v1beta1/options/snapshots/AAPL"))
        .and(query_param("limit", "1"))
        .and(query_param_is_missing("page_token"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "snapshots": {
                "AAPL260406C00180000": {
                    "latestQuote": { "bp": 73.95 }
                }
            },
            "next_page_token": "page-2"
        })))
        .mount(&server)
        .await;

    Mock::given(method("GET"))
        .and(path("/v1beta1/options/snapshots/AAPL"))
        .and(query_param("limit", "1"))
        .and(query_param("page_token", "page-2"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "snapshots": {
                "AAPL260406C00180000": {
                    "latestQuote": { "bp": 74.00 }
                }
            },
            "next_page_token": null
        })))
        .mount(&server)
        .await;

    let pages = authed_client(server.uri())
        .options()
        .chain_stream(options::ChainRequest {
            underlying_symbol: "AAPL".into(),
            limit: Some(1),
            ..Default::default()
        })
        .collect::<Vec<_>>()
        .await;

    assert_eq!(pages.len(), 2);
    assert!(pages[0].as_ref().is_ok());
    assert!(matches!(pages[1], Err(Error::Pagination(_))));
}