bitpanda-api 0.1.0

Rust client for Bitpanda API
Documentation
use std::str::FromStr;

use chrono::{DateTime, FixedOffset};
use lazy_regex::regex;
use rust_decimal::Decimal;

use super::ApiResult;

use crate::{
    model::{Trade, TradeStatus, TradeType},
    ApiError,
};

#[derive(Deserialize)]
pub struct TradeResponse {
    data: Vec<Data>,
    links: Links,
}

impl TradeResponse {
    pub fn next_page(&self) -> Option<usize> {
        if let Some(next) = &self.links.next {
            let rgx = regex!(r"page=([0-9]+)");
            rgx.captures(next).and_then(|captures| {
                captures
                    .get(1)
                    .map(|capture| capture.as_str().parse().unwrap())
            })
        } else {
            None
        }
    }
}

#[derive(Deserialize)]
struct Links {
    next: Option<String>,
}

#[derive(Deserialize)]
struct Data {
    attributes: Attributes,
    id: String,
}

#[derive(Deserialize)]
struct Attributes {
    status: String,
    r#type: String,
    cryptocoin_id: String,
    cryptocoin_symbol: String,
    fiat_id: String,
    amount_fiat: Decimal,
    amount_cryptocoin: Decimal,
    fiat_to_eur_rate: Decimal,
    wallet_id: String,
    fiat_wallet_id: Option<String>,
    related_swap_trade: Option<Box<Data>>,
    time: Time,
    price: Decimal,
}

#[derive(Deserialize)]
struct Time {
    date_iso8601: DateTime<FixedOffset>,
}

impl TradeResponse {
    pub fn into_trades(self) -> ApiResult<Vec<Trade>> {
        let mut trades = Vec::with_capacity(self.data.len());
        for trade in self.data.into_iter() {
            trades.push(Trade::try_from(trade)?)
        }

        Ok(trades)
    }
}

impl TryFrom<Data> for Trade {
    type Error = ApiError;

    fn try_from(trade: Data) -> Result<Self, Self::Error> {
        let related_swap = match trade.attributes.related_swap_trade {
            None => None,
            Some(data) => {
                let trade = Trade::try_from(*data)?;
                Some(Box::new(trade))
            }
        };

        Ok(Trade {
            amount_asset: trade.attributes.amount_cryptocoin,
            amount_fiat: trade.attributes.amount_fiat,
            datetime: trade.attributes.time.date_iso8601,
            fiat_to_eur_rate: trade.attributes.fiat_to_eur_rate,
            fiat_wallet_id: trade.attributes.fiat_wallet_id,
            id_asset: trade.attributes.cryptocoin_id,
            id_fiat: trade.attributes.fiat_id,
            id_wallet: trade.attributes.wallet_id,
            id: trade.id,
            price: trade.attributes.price,
            related_swap_trade: related_swap,
            status: TradeStatus::from_str(&trade.attributes.status)?,
            symbol: trade.attributes.cryptocoin_symbol,
            r#type: TradeType::from_str(&trade.attributes.r#type)?,
        })
    }
}

#[cfg(test)]
mod test {
    use super::*;

    use pretty_assertions::assert_eq;

    #[test]
    fn should_parse_next_page() {
        let response = TradeResponse {
            data: vec![],
            links: Links {
                next: Some("?page=18&page_size=2".to_string()),
            },
        };

        assert_eq!(response.next_page(), Some(18));

        let response = TradeResponse {
            data: vec![],
            links: Links { next: None },
        };

        assert!(response.next_page().is_none());
    }
}