kalshi-pmx 0.1.1

Async Rust client for Kalshi's Trade API and WebSocket streams.
Documentation
use kalshi_pmx::ws::Channel;

#[test]
fn openapi_operation_ids_are_accounted_for() {
    let Some(openapi) = repo_spec("openapi.yaml") else {
        eprintln!("skipping OpenAPI coverage check: repo-only specs are not packaged");
        return;
    };
    let operations = openapi
        .lines()
        .filter_map(|line| line.trim().strip_prefix("operationId: "))
        .map(str::to_owned)
        .collect::<Vec<_>>();

    let supported = [
        "GetExchangeStatus",
        "GetExchangeAnnouncements",
        "GetSeriesFeeChanges",
        "GetExchangeSchedule",
        "GetUserDataTimestamp",
        "GetMarketCandlesticks",
        "GetTrades",
        "GetMarketOrderbook",
        "GetMarketOrderbooks",
        "GetSeries",
        "GetSeriesList",
        "GetMarkets",
        "GetMarket",
        "BatchGetMarketCandlesticks",
        "GetMarketCandlesticksByEvent",
        "GetEvents",
        "GetMultivariateEvents",
        "GetEventFeeChanges",
        "GetEvent",
        "GetEventMetadata",
        "GetEventForecastPercentilesHistory",
        "GetOrders",
        "GetOrder",
        "GetOrderQueuePositions",
        "GetOrderQueuePosition",
        "CreateOrderV2",
        "BatchCreateOrdersV2",
        "BatchCancelOrdersV2",
        "CancelOrderV2",
        "AmendOrderV2",
        "DecreaseOrderV2",
        "GetOrderGroups",
        "CreateOrderGroup",
        "GetOrderGroup",
        "DeleteOrderGroup",
        "ResetOrderGroup",
        "TriggerOrderGroup",
        "UpdateOrderGroupLimit",
        "GetBalance",
        "CreateSubaccount",
        "ApplySubaccountTransfer",
        "GetSubaccountBalances",
        "GetSubaccountTransfers",
        "UpdateSubaccountNetting",
        "GetSubaccountNetting",
        "GetPositions",
        "GetSettlements",
        "GetDeposits",
        "GetWithdrawals",
        "GetPortfolioRestingOrderTotalValue",
        "GetFills",
        "GetCommunicationsID",
        "GetBlockTradeProposals",
        "ProposeBlockTrade",
        "AcceptBlockTradeProposal",
        "GetRFQs",
        "CreateRFQ",
        "GetRFQ",
        "DeleteRFQ",
        "GetQuotes",
        "CreateQuote",
        "GetQuote",
        "DeleteQuote",
        "AcceptQuote",
        "ConfirmQuote",
        "GetApiKeys",
        "CreateApiKey",
        "GenerateApiKey",
        "DeleteApiKey",
        "GetAccountApiLimits",
        "UpgradeAccountApiUsageLevel",
        "GetAccountApiUsageLevelVolumeProgress",
        "GetAccountEndpointCosts",
        "GetTagsForSeriesCategories",
        "GetFiltersForSports",
        "GetLiveDataByMilestone",
        "GetLiveData",
        "GetLiveDatas",
        "GetGameStats",
        "GetStructuredTargets",
        "GetStructuredTarget",
        "GetMilestone",
        "GetMilestones",
        "GetMultivariateEventCollection",
        "CreateMarketInMultivariateEventCollection",
        "GetMultivariateEventCollections",
        "LookupTickersForMarketInMultivariateEventCollection",
        "GetMultivariateEventCollectionLookupHistory",
        "GetIncentivePrograms",
        "GetFCMOrders",
        "GetFCMPositions",
        "GetHistoricalCutoff",
        "GetMarketCandlesticksHistorical",
        "GetFillsHistorical",
        "GetHistoricalOrders",
        "GetTradesHistorical",
        "GetHistoricalMarkets",
        "GetHistoricalMarket",
    ]
    .into_iter()
    .map(str::to_owned)
    .collect::<Vec<_>>();

    assert_eq!(operations, supported);

    let typed_method_count = include_str!("../src/typed.rs")
        .matches("pub async fn ")
        .count();
    assert_eq!(typed_method_count, operations.len());
}

#[test]
fn openapi_component_schemas_are_generated() {
    let generated_type_count = include_str!("../src/generated.rs")
        .lines()
        .filter(|line| {
            line.starts_with("pub struct ")
                || line.starts_with("pub type ")
                || line.starts_with("pub enum ")
        })
        .count();

    assert_eq!(generated_type_count, 181);
}

#[test]
fn asyncapi_channels_are_accounted_for() {
    let Some(asyncapi) = repo_spec("asyncapi.yaml") else {
        eprintln!("skipping AsyncAPI coverage check: repo-only specs are not packaged");
        return;
    };
    let channels = asyncapi_channels(&asyncapi);
    assert_eq!(
        channels,
        [
            "root",
            "control_frames",
            "orderbook_delta",
            "ticker",
            "trade",
            "fill",
            "market_positions",
            "market_lifecycle_v2",
            "multivariate_market_lifecycle",
            "multivariate",
            "communications",
            "order_group_updates",
            "user_orders",
            "cfbenchmarks_value",
        ]
    );

    let subscription_channels = [
        Channel::OrderbookDelta,
        Channel::Ticker,
        Channel::Trade,
        Channel::Fill,
        Channel::MarketPositions,
        Channel::MarketLifecycleV2,
        Channel::MultivariateMarketLifecycle,
        Channel::Multivariate,
        Channel::Communications,
        Channel::OrderGroupUpdates,
        Channel::UserOrders,
        Channel::CfBenchmarksValue,
    ]
    .map(|channel| serde_json::to_value(channel).unwrap());

    assert_eq!(
        subscription_channels,
        [
            "orderbook_delta",
            "ticker",
            "trade",
            "fill",
            "market_positions",
            "market_lifecycle_v2",
            "multivariate_market_lifecycle",
            "multivariate",
            "communications",
            "order_group_updates",
            "user_orders",
            "cfbenchmarks_value",
        ]
        .map(serde_json::Value::from)
    );
}

#[test]
fn rest_namespaces_stay_typed_and_high_level() {
    for entry in
        std::fs::read_dir(std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/rest"))
            .unwrap()
    {
        let path = entry.unwrap().path();
        if path.file_name().and_then(|name| name.to_str()) == Some("mod.rs") {
            continue;
        }

        let source = std::fs::read_to_string(&path).unwrap();
        assert!(
            !source.contains("raw_"),
            "{} should not call hidden raw request helpers",
            path.display()
        );
        assert!(
            !source.contains("request_json"),
            "{} should delegate through the typed OpenAPI layer",
            path.display()
        );
        assert!(
            source.contains(".typed()"),
            "{} should delegate to generated typed operations",
            path.display()
        );
    }
}

fn repo_spec(file_name: &str) -> Option<String> {
    std::fs::read_to_string(
        std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("specs")
            .join(file_name),
    )
    .ok()
}

fn asyncapi_channels(spec: &str) -> Vec<&str> {
    let mut channels = Vec::new();
    let mut in_channels = false;

    for line in spec.lines() {
        if line == "channels:" {
            in_channels = true;
            continue;
        }

        if line == "operations:" {
            break;
        }

        if in_channels && line.starts_with("  ") && !line.starts_with("    ") && line.ends_with(':')
        {
            channels.push(line.trim().trim_end_matches(':'));
        }
    }

    channels
}