delphi/sources/
dunamu.rs

1//! Dunamu Source Provider (v0.4 API)
2//! <https://www.dunamu.com/>
3
4use super::Price;
5use crate::{
6    config::HttpsConfig,
7    error::{Error, ErrorKind},
8    prelude::*,
9    Currency, TradingPair,
10};
11use iqhttp::{HttpsClient, Query};
12use rust_decimal::Decimal;
13use serde::{Deserialize, Serialize};
14
15//https://quotation-api-cdn.dunamu.com/v1/forex/recent?codes=FRX.KRWUSD
16
17/// Base URI for requests to the Dunamu API
18pub const API_HOST: &str = "quotation-api-cdn.dunamu.com";
19
20/// Source provider for Dunamu
21pub struct DunamuSource {
22    https_client: HttpsClient,
23}
24
25impl DunamuSource {
26    /// Create a new Dunamu source provider
27    #[allow(clippy::new_without_default)]
28    pub fn new(config: &HttpsConfig) -> Result<Self, Error> {
29        let https_client = config.new_client(API_HOST)?;
30        Ok(Self { https_client })
31    }
32
33    /// Get trading pairs
34    pub async fn trading_pairs(&self, pair: &TradingPair) -> Result<Price, Error> {
35        if pair.0 != Currency::Krw && pair.1 != Currency::Krw {
36            fail!(ErrorKind::Currency, "trading pair must be with KRW");
37        }
38
39        let mut query = Query::new();
40        query.add("codes", format!("FRX.{}{}", pair.0, pair.1));
41
42        let api_response: Response = self
43            .https_client
44            .get_json("/v1/forex/recent", &query)
45            .await?;
46        let price: Decimal = api_response[0].base_price.to_string().parse()?;
47        Ok(Price::new(price)?)
48    }
49}
50
51/// API responses Vector
52pub type Response = Vec<ResponseElement>;
53/// API response entity
54#[derive(Serialize, Deserialize)]
55#[serde(rename_all = "camelCase")]
56pub struct ResponseElement {
57    code: String,
58    currency_code: String,
59    currency_name: String,
60    country: String,
61    name: String,
62    date: String,
63    time: String,
64    recurrence_count: i64,
65    base_price: f64,
66    opening_price: f64,
67    high_price: f64,
68    low_price: f64,
69    change: String,
70    change_price: f64,
71    cash_buying_price: f64,
72    cash_selling_price: f64,
73    tt_buying_price: f64,
74    tt_selling_price: f64,
75    tc_buying_price: Option<serde_json::Value>,
76    fc_selling_price: Option<serde_json::Value>,
77    exchange_commission: f64,
78    us_dollar_rate: f64,
79    #[serde(rename = "high52wPrice")]
80    high52_w_price: f64,
81    #[serde(rename = "high52wDate")]
82    high52_w_date: String,
83    #[serde(rename = "low52wPrice")]
84    low52_w_price: f64,
85    #[serde(rename = "low52wDate")]
86    low52_w_date: String,
87    currency_unit: i64,
88    provider: String,
89    timestamp: i64,
90    id: i64,
91    created_at: String,
92    modified_at: String,
93    change_rate: f64,
94    signed_change_price: f64,
95    signed_change_rate: f64,
96}
97
98#[cfg(test)]
99mod tests {
100    use super::DunamuSource;
101
102    /// `trading_pairs()` test with known currency pair
103    #[tokio::test]
104    #[ignore]
105    async fn trading_pairs_ok() {
106        let pair = "KRW/USD".parse().unwrap();
107        let _response = DunamuSource::new(&Default::default())
108            .unwrap()
109            .trading_pairs(&pair)
110            .await
111            .unwrap();
112    }
113
114    /// `trading_pairs()` with invalid currency pair
115    #[tokio::test]
116    #[ignore]
117    async fn trading_pairs_404() {
118        let pair = "N/A".parse().unwrap();
119
120        // TODO(tarcieri): test 404 handling
121        let _err = DunamuSource::new(&Default::default())
122            .unwrap()
123            .trading_pairs(&pair)
124            .await
125            .err()
126            .unwrap();
127    }
128}