alpaca-mock 0.30.0

Executable mock server for alpaca-rust trading and market-data flows
Documentation
use std::collections::HashSet;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};

use alpaca_trade::assets::Asset;
use alpaca_trade::watchlists::{Watchlist, WatchlistSummary};

use super::{MockStateError, assets, now_string};

#[derive(Debug, Clone, Default)]
pub(super) struct WatchlistBook {
    entries: Vec<Watchlist>,
    sequence: u64,
}

impl WatchlistBook {
    pub(super) fn summaries(&self) -> Vec<WatchlistSummary> {
        self.entries
            .iter()
            .map(|watchlist| WatchlistSummary {
                id: watchlist.id.clone(),
                account_id: watchlist.account_id.clone(),
                created_at: watchlist.created_at.clone(),
                updated_at: watchlist.updated_at.clone(),
                name: watchlist.name.clone(),
            })
            .collect()
    }

    pub(super) fn create(
        &mut self,
        account_id: &str,
        name: String,
        symbols: Option<Vec<String>>,
    ) -> Result<Watchlist, MockStateError> {
        self.ensure_unique_name(&name, None)?;
        let assets = resolve_assets(symbols.unwrap_or_default())?;
        let now = now_string();
        let watchlist = Watchlist {
            id: self.next_id(account_id),
            account_id: account_id.to_owned(),
            created_at: now.clone(),
            updated_at: now,
            name,
            assets: Some(assets),
        };
        self.entries.push(watchlist.clone());
        Ok(watchlist)
    }

    pub(super) fn get_by_id(&self, watchlist_id: &str) -> Result<Watchlist, MockStateError> {
        self.entries
            .iter()
            .find(|watchlist| watchlist.id == watchlist_id)
            .cloned()
            .ok_or_else(|| watchlist_not_found_by_id(watchlist_id))
    }

    pub(super) fn get_by_name(&self, name: &str) -> Result<Watchlist, MockStateError> {
        self.entries
            .iter()
            .find(|watchlist| watchlist.name == name)
            .cloned()
            .ok_or_else(|| watchlist_not_found_by_name(name))
    }

    pub(super) fn update_by_id(
        &mut self,
        watchlist_id: &str,
        name: Option<String>,
        symbols: Option<Vec<String>>,
    ) -> Result<Watchlist, MockStateError> {
        let index = self
            .entries
            .iter()
            .position(|watchlist| watchlist.id == watchlist_id)
            .ok_or_else(|| watchlist_not_found_by_id(watchlist_id))?;
        self.update(index, name, symbols)
    }

    pub(super) fn update_by_name(
        &mut self,
        current_name: &str,
        name: Option<String>,
        symbols: Option<Vec<String>>,
    ) -> Result<Watchlist, MockStateError> {
        let index = self
            .entries
            .iter()
            .position(|watchlist| watchlist.name == current_name)
            .ok_or_else(|| watchlist_not_found_by_name(current_name))?;
        self.update(index, name, symbols)
    }

    pub(super) fn delete_by_id(&mut self, watchlist_id: &str) -> Result<(), MockStateError> {
        let index = self
            .entries
            .iter()
            .position(|watchlist| watchlist.id == watchlist_id)
            .ok_or_else(|| watchlist_not_found_by_id(watchlist_id))?;
        self.entries.remove(index);
        Ok(())
    }

    pub(super) fn delete_by_name(&mut self, name: &str) -> Result<(), MockStateError> {
        let index = self
            .entries
            .iter()
            .position(|watchlist| watchlist.name == name)
            .ok_or_else(|| watchlist_not_found_by_name(name))?;
        self.entries.remove(index);
        Ok(())
    }

    pub(super) fn add_asset_by_id(
        &mut self,
        watchlist_id: &str,
        symbol: &str,
    ) -> Result<Watchlist, MockStateError> {
        let index = self
            .entries
            .iter()
            .position(|watchlist| watchlist.id == watchlist_id)
            .ok_or_else(|| watchlist_not_found_by_id(watchlist_id))?;
        self.add_asset(index, symbol)
    }

    pub(super) fn add_asset_by_name(
        &mut self,
        name: &str,
        symbol: &str,
    ) -> Result<Watchlist, MockStateError> {
        let index = self
            .entries
            .iter()
            .position(|watchlist| watchlist.name == name)
            .ok_or_else(|| watchlist_not_found_by_name(name))?;
        self.add_asset(index, symbol)
    }

    pub(super) fn remove_asset_by_id(
        &mut self,
        watchlist_id: &str,
        symbol: &str,
    ) -> Result<Watchlist, MockStateError> {
        let watchlist = self
            .entries
            .iter_mut()
            .find(|watchlist| watchlist.id == watchlist_id)
            .ok_or_else(|| watchlist_not_found_by_id(watchlist_id))?;
        let assets = watchlist.assets.get_or_insert_default();
        let index = assets
            .iter()
            .position(|asset| asset.symbol.eq_ignore_ascii_case(symbol))
            .ok_or_else(|| {
                MockStateError::NotFound(format!(
                    "symbol {symbol} was not found in watchlist {watchlist_id}"
                ))
            })?;
        assets.remove(index);
        watchlist.updated_at = now_string();
        Ok(watchlist.clone())
    }

    fn update(
        &mut self,
        index: usize,
        name: Option<String>,
        symbols: Option<Vec<String>>,
    ) -> Result<Watchlist, MockStateError> {
        if let Some(name) = name.as_deref() {
            self.ensure_unique_name(name, Some(index))?;
        }
        let assets = symbols.map(resolve_assets).transpose()?;
        let watchlist = &mut self.entries[index];
        if let Some(name) = name {
            watchlist.name = name;
        }
        if let Some(assets) = assets {
            watchlist.assets = Some(assets);
        }
        watchlist.updated_at = now_string();
        Ok(watchlist.clone())
    }

    fn add_asset(&mut self, index: usize, symbol: &str) -> Result<Watchlist, MockStateError> {
        let asset = resolve_asset(symbol)?;
        let watchlist = &mut self.entries[index];
        let assets = watchlist.assets.get_or_insert_default();
        if assets
            .iter()
            .any(|existing| existing.symbol.eq_ignore_ascii_case(&asset.symbol))
        {
            return Err(MockStateError::Conflict(format!(
                "symbol {} already exists in watchlist {}",
                asset.symbol, watchlist.name
            )));
        }
        assets.push(asset);
        watchlist.updated_at = now_string();
        Ok(watchlist.clone())
    }

    fn ensure_unique_name(
        &self,
        name: &str,
        excluded_index: Option<usize>,
    ) -> Result<(), MockStateError> {
        if self
            .entries
            .iter()
            .enumerate()
            .any(|(index, watchlist)| Some(index) != excluded_index && watchlist.name == name)
        {
            return Err(MockStateError::Conflict(format!(
                "watchlist name {name} already exists"
            )));
        }
        Ok(())
    }

    fn next_id(&mut self, account_id: &str) -> String {
        self.sequence = self.sequence.wrapping_add(1);
        let mut hasher = DefaultHasher::new();
        account_id.hash(&mut hasher);
        self.sequence.hash(&mut hasher);
        let suffix = hasher.finish() & 0x0000_ffff_ffff_ffff;
        format!("00000000-0000-4000-8000-{suffix:012x}")
    }
}

fn resolve_assets(symbols: Vec<String>) -> Result<Vec<Asset>, MockStateError> {
    let mut seen = HashSet::new();
    symbols
        .into_iter()
        .map(|symbol| {
            let asset = resolve_asset(&symbol)?;
            if !seen.insert(asset.symbol.clone()) {
                return Err(MockStateError::Conflict(format!(
                    "symbol {} appears more than once",
                    asset.symbol
                )));
            }
            Ok(asset)
        })
        .collect()
}

fn resolve_asset(symbol: &str) -> Result<Asset, MockStateError> {
    assets::catalog()
        .into_iter()
        .find(|asset| asset.symbol.eq_ignore_ascii_case(symbol))
        .ok_or_else(|| MockStateError::NotFound(format!("asset {symbol} was not found")))
}

fn watchlist_not_found_by_id(watchlist_id: &str) -> MockStateError {
    MockStateError::NotFound(format!("watchlist {watchlist_id} was not found"))
}

fn watchlist_not_found_by_name(name: &str) -> MockStateError {
    MockStateError::NotFound(format!("watchlist named {name} was not found"))
}