alpaca-mock 0.30.0

Executable mock server for alpaca-rust trading and market-data flows
Documentation
use axum::{
    Router,
    body::Body,
    extract::State,
    http::{HeaderValue, Request, header::HeaderName},
    middleware::{self, Next},
    response::Response,
    routing::{get, post},
};
use std::sync::atomic::{AtomicU64, Ordering};

use crate::auth::{AuthenticatedAccount, MockHttpError, extract_auth};
use crate::handlers;
use crate::state::{MarketDataBridgeError, MockServerState};

static MOCK_REQUEST_ID: AtomicU64 = AtomicU64::new(1);

pub fn build_app() -> Router {
    build_app_with_state(MockServerState::new())
}

pub fn build_app_from_env() -> Result<Router, MarketDataBridgeError> {
    Ok(build_app_with_state(MockServerState::from_env()?))
}

pub fn build_app_with_state(state: MockServerState) -> Router {
    let trading_router = Router::new()
        .route("/v2/account", get(handlers::account_get))
        .route(
            "/v2/account/configurations",
            get(handlers::account_configurations_get)
                .patch(handlers::account_configurations_update),
        )
        .route(
            "/v2/account/portfolio/history",
            get(handlers::portfolio_history_get),
        )
        .route("/v2/assets", get(handlers::assets_list))
        .route("/v2/assets/{symbol_or_asset_id}", get(handlers::assets_get))
        .route(
            "/v2/options/contracts",
            get(handlers::options_contracts_list),
        )
        .route(
            "/v2/options/contracts/{symbol_or_id}",
            get(handlers::options_contracts_get),
        )
        .route("/v2/calendar", get(handlers::calendar_legacy))
        .route("/v3/calendar/{market}", get(handlers::calendar_v3))
        .route("/v2/clock", get(handlers::clock_legacy))
        .route("/v3/clock", get(handlers::clock_v3))
        .route("/v2/account/activities", get(handlers::activities_list))
        .route(
            "/v2/account/activities/{activity_type}",
            get(handlers::activities_by_type),
        )
        .route(
            "/v2/orders",
            get(handlers::orders_list)
                .post(handlers::orders_create)
                .delete(handlers::orders_cancel_all),
        )
        .route(
            "/v2/orders/{order_id}",
            get(handlers::orders_get)
                .patch(handlers::orders_replace)
                .delete(handlers::orders_cancel),
        )
        .route(
            "/v2/orders:by_client_order_id",
            get(handlers::orders_get_by_client_order_id),
        )
        .route(
            "/v2/watchlists",
            get(handlers::watchlists_list).post(handlers::watchlists_create),
        )
        .route(
            "/v2/watchlists:by_name",
            get(handlers::watchlists_get_by_name)
                .put(handlers::watchlists_update_by_name)
                .post(handlers::watchlists_add_asset_by_name)
                .delete(handlers::watchlists_delete_by_name),
        )
        .route(
            "/v2/watchlists/{watchlist_id}",
            get(handlers::watchlists_get_by_id)
                .put(handlers::watchlists_update_by_id)
                .post(handlers::watchlists_add_asset_by_id)
                .delete(handlers::watchlists_delete_by_id),
        )
        .route(
            "/v2/watchlists/{watchlist_id}/{symbol}",
            axum::routing::delete(handlers::watchlists_remove_asset_by_id),
        )
        .route(
            "/v2/positions",
            get(handlers::positions_list).delete(handlers::positions_close_all),
        )
        .route(
            "/v2/positions/{symbol_or_asset_id}",
            get(handlers::positions_get).delete(handlers::positions_close),
        )
        .route(
            "/v2/positions/{symbol_or_contract_id}/exercise",
            post(handlers::positions_exercise),
        )
        .route(
            "/v2/positions/{symbol_or_contract_id}/do-not-exercise",
            post(handlers::positions_do_not_exercise),
        )
        .route_layer(middleware::from_fn_with_state(
            state.clone(),
            require_trading_auth,
        ));

    Router::new()
        .route("/health", get(handlers::health))
        .route("/reset", post(handlers::admin_reset))
        .route("/admin/state", get(handlers::admin_state))
        .route("/admin/reset", post(handlers::admin_reset))
        .route("/admin/faults/http", post(handlers::admin_set_http_fault))
        .merge(trading_router)
        .with_state(state)
}

async fn require_trading_auth(
    State(state): State<MockServerState>,
    mut request: Request<Body>,
    next: Next,
) -> Result<Response, MockHttpError> {
    let auth = extract_auth(request.headers())?;
    let _secret_key = auth.secret_key;
    state.ensure_account(&auth.api_key);

    if let Some(fault) = state.take_http_fault() {
        let status = fault.status_code().map_err(MockHttpError::bad_request)?;
        return Err(MockHttpError::with_status(status, fault.message));
    }

    request.extensions_mut().insert(AuthenticatedAccount {
        api_key: auth.api_key,
    });

    let mut response = next.run(request).await;
    let request_id = MOCK_REQUEST_ID.fetch_add(1, Ordering::Relaxed);
    response.headers_mut().insert(
        HeaderName::from_static("x-request-id"),
        HeaderValue::from_str(&format!("mock-{request_id:016x}"))
            .expect("generated mock request id should be a valid header value"),
    );

    Ok(response)
}