alpaca-mock 0.30.0

Executable mock server for alpaca-rust trading and market-data flows
Documentation
use alpaca_trade::watchlists::{
    AddAssetRequest, CreateRequest, UpdateRequest, Watchlist, WatchlistSummary,
};
use axum::{
    Json,
    extract::{Extension, Path, Query, State},
    http::StatusCode,
};
use serde::Deserialize;

use crate::auth::{AuthenticatedAccount, MockHttpError};
use crate::state::{MockServerState, MockStateError};

type RouteResult<T> = Result<T, MockHttpError>;

#[derive(Debug, Deserialize)]
pub(crate) struct WatchlistNameQuery {
    name: String,
}

pub(crate) async fn watchlists_list(
    State(state): State<MockServerState>,
    Extension(account): Extension<AuthenticatedAccount>,
) -> Json<Vec<WatchlistSummary>> {
    Json(state.list_watchlists(&account.api_key))
}

pub(crate) async fn watchlists_create(
    State(state): State<MockServerState>,
    Extension(account): Extension<AuthenticatedAccount>,
    Json(request): Json<CreateRequest>,
) -> RouteResult<Json<Watchlist>> {
    request.validate().map_err(invalid_request)?;
    state
        .create_watchlist(&account.api_key, request.name, request.symbols)
        .map(Json)
        .map_err(state_error)
}

pub(crate) async fn watchlists_get_by_id(
    State(state): State<MockServerState>,
    Extension(account): Extension<AuthenticatedAccount>,
    Path(watchlist_id): Path<String>,
) -> RouteResult<Json<Watchlist>> {
    state
        .get_watchlist_by_id(&account.api_key, &watchlist_id)
        .map(Json)
        .map_err(state_error)
}

pub(crate) async fn watchlists_update_by_id(
    State(state): State<MockServerState>,
    Extension(account): Extension<AuthenticatedAccount>,
    Path(watchlist_id): Path<String>,
    Json(request): Json<UpdateRequest>,
) -> RouteResult<Json<Watchlist>> {
    request.validate().map_err(invalid_request)?;
    state
        .update_watchlist_by_id(
            &account.api_key,
            &watchlist_id,
            request.name,
            request.symbols,
        )
        .map(Json)
        .map_err(state_error)
}

pub(crate) async fn watchlists_delete_by_id(
    State(state): State<MockServerState>,
    Extension(account): Extension<AuthenticatedAccount>,
    Path(watchlist_id): Path<String>,
) -> RouteResult<StatusCode> {
    state
        .delete_watchlist_by_id(&account.api_key, &watchlist_id)
        .map_err(state_error)?;
    Ok(StatusCode::NO_CONTENT)
}

pub(crate) async fn watchlists_add_asset_by_id(
    State(state): State<MockServerState>,
    Extension(account): Extension<AuthenticatedAccount>,
    Path(watchlist_id): Path<String>,
    Json(request): Json<AddAssetRequest>,
) -> RouteResult<Json<Watchlist>> {
    request.validate().map_err(invalid_request)?;
    state
        .add_watchlist_asset_by_id(&account.api_key, &watchlist_id, &request.symbol)
        .map(Json)
        .map_err(state_error)
}

pub(crate) async fn watchlists_remove_asset_by_id(
    State(state): State<MockServerState>,
    Extension(account): Extension<AuthenticatedAccount>,
    Path((watchlist_id, symbol)): Path<(String, String)>,
) -> RouteResult<Json<Watchlist>> {
    state
        .remove_watchlist_asset_by_id(&account.api_key, &watchlist_id, &symbol)
        .map(Json)
        .map_err(state_error)
}

pub(crate) async fn watchlists_get_by_name(
    State(state): State<MockServerState>,
    Extension(account): Extension<AuthenticatedAccount>,
    Query(query): Query<WatchlistNameQuery>,
) -> RouteResult<Json<Watchlist>> {
    validate_name(&query.name)?;
    state
        .get_watchlist_by_name(&account.api_key, &query.name)
        .map(Json)
        .map_err(state_error)
}

pub(crate) async fn watchlists_update_by_name(
    State(state): State<MockServerState>,
    Extension(account): Extension<AuthenticatedAccount>,
    Query(query): Query<WatchlistNameQuery>,
    Json(request): Json<UpdateRequest>,
) -> RouteResult<Json<Watchlist>> {
    validate_name(&query.name)?;
    request.validate().map_err(invalid_request)?;
    state
        .update_watchlist_by_name(&account.api_key, &query.name, request.name, request.symbols)
        .map(Json)
        .map_err(state_error)
}

pub(crate) async fn watchlists_add_asset_by_name(
    State(state): State<MockServerState>,
    Extension(account): Extension<AuthenticatedAccount>,
    Query(query): Query<WatchlistNameQuery>,
    Json(request): Json<AddAssetRequest>,
) -> RouteResult<Json<Watchlist>> {
    validate_name(&query.name)?;
    request.validate().map_err(invalid_request)?;
    state
        .add_watchlist_asset_by_name(&account.api_key, &query.name, &request.symbol)
        .map(Json)
        .map_err(state_error)
}

pub(crate) async fn watchlists_delete_by_name(
    State(state): State<MockServerState>,
    Extension(account): Extension<AuthenticatedAccount>,
    Query(query): Query<WatchlistNameQuery>,
) -> RouteResult<StatusCode> {
    validate_name(&query.name)?;
    state
        .delete_watchlist_by_name(&account.api_key, &query.name)
        .map_err(state_error)?;
    Ok(StatusCode::NO_CONTENT)
}

fn validate_name(name: &str) -> RouteResult<()> {
    UpdateRequest {
        name: Some(name.to_owned()),
        symbols: None,
    }
    .validate()
    .map_err(invalid_request)
}

fn invalid_request(error: alpaca_trade::Error) -> MockHttpError {
    MockHttpError::conflict(error.to_string())
}

fn state_error(error: MockStateError) -> MockHttpError {
    match error {
        MockStateError::NotFound(message) => MockHttpError::not_found(message),
        MockStateError::Forbidden(message) => {
            MockHttpError::with_status(StatusCode::FORBIDDEN, message)
        }
        MockStateError::Conflict(message) => MockHttpError::conflict(message),
        MockStateError::MarketDataUnavailable(message) => MockHttpError::internal(message),
    }
}