ticksupply 0.2.1

Official Rust client for the Ticksupply market data API
Documentation
mod common;

use common::mock_client;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use ticksupply::Error;
use wiremock::matchers::{method, path};
use wiremock::{Mock, Request, ResponseTemplate};

fn count_responder(
    counter: Arc<AtomicUsize>,
    fail_first_n: usize,
    then_status: u16,
    then_body: serde_json::Value,
    fail_status: u16,
    fail_body: serde_json::Value,
) -> impl Fn(&Request) -> ResponseTemplate + Send + Sync + 'static {
    move |_req: &Request| {
        let n = counter.fetch_add(1, Ordering::SeqCst);
        if n < fail_first_n {
            ResponseTemplate::new(fail_status).set_body_json(&fail_body)
        } else {
            ResponseTemplate::new(then_status).set_body_json(&then_body)
        }
    }
}

#[tokio::test]
async fn get_is_retried_on_5xx() {
    let server = wiremock::MockServer::start().await;
    let client = ticksupply::Client::builder()
        .api_key("k")
        .base_url(format!("{}/v1", server.uri()))
        .max_retries(2)
        .build()
        .unwrap();

    let counter = Arc::new(AtomicUsize::new(0));
    Mock::given(method("GET"))
        .and(path("/v1/exchanges"))
        .respond_with(count_responder(
            counter.clone(),
            2,
            200,
            serde_json::json!([]),
            503,
            serde_json::json!({ "error": { "code": "server_error", "message": "boom" } }),
        ))
        .mount(&server)
        .await;

    let result = client.exchanges().list().await;
    assert!(result.is_ok(), "expected ok after retries: {result:?}");
    assert_eq!(counter.load(Ordering::SeqCst), 3);
}

#[tokio::test]
async fn post_without_idempotency_key_is_not_retried() {
    let (server, _client) = mock_client().await;
    // Rebuild client with retries enabled.
    let client = ticksupply::Client::builder()
        .api_key("k")
        .base_url(format!("{}/v1", server.uri()))
        .max_retries(3)
        .build()
        .unwrap();

    let counter = Arc::new(AtomicUsize::new(0));
    Mock::given(method("POST"))
        .and(path("/v1/subscriptions"))
        .respond_with(count_responder(
            counter.clone(),
            10,
            200,
            serde_json::json!({}),
            503,
            serde_json::json!({ "error": { "code": "server_error", "message": "boom" } }),
        ))
        .mount(&server)
        .await;

    let err = client.subscriptions().create(42).send().await.unwrap_err();
    assert!(matches!(err, Error::Api { status: 503, .. }));
    assert_eq!(
        counter.load(Ordering::SeqCst),
        1,
        "POST should not retry without idempotency key"
    );
}

#[tokio::test]
async fn post_with_idempotency_key_retries() {
    let server = wiremock::MockServer::start().await;
    let client = ticksupply::Client::builder()
        .api_key("k")
        .base_url(format!("{}/v1", server.uri()))
        .max_retries(2)
        .build()
        .unwrap();

    let counter = Arc::new(AtomicUsize::new(0));
    Mock::given(method("POST"))
        .and(path("/v1/subscriptions"))
        .respond_with(count_responder(
            counter.clone(),
            2,
            200,
            serde_json::json!({
                "id": "sub_x",
                "status": "active",
                "datastream": {
                    "datastream_id": 42,
                    "exchange": "binance",
                    "instrument": "BTCUSDT",
                    "stream_type": "trades",
                    "wire_format": "json"
                },
                "created_at": "2024-01-01T00:00:00Z",
                "spans": []
            }),
            503,
            serde_json::json!({ "error": { "code": "server_error", "message": "boom" } }),
        ))
        .mount(&server)
        .await;

    let sub = client
        .subscriptions()
        .create(42)
        .idempotency_key("00000000-0000-4000-8000-000000000000")
        .send()
        .await
        .unwrap();
    assert_eq!(sub.id, "sub_x");
    assert_eq!(counter.load(Ordering::SeqCst), 3);
}