stock-rust 0.1.0

Rust version of stock-api
Documentation
use anyhow::Result;
use async_trait::async_trait;
use serde_json::Value;

use crate::stocks::base::{
    as_f64, as_string, dedup_codes, percent,
};
use crate::stocks::transforms::base::CommonCodeTransform;
use crate::stocks::transforms::netease::{NeteaseCommonCodeTransform, NeteaseSearchCodeTransform};
use crate::types::{Stock, StockApi};

#[derive(Clone)]
pub struct NeteaseApi {
    client: reqwest::Client,
}

impl NeteaseApi {
    pub fn new(client: reqwest::Client) -> Self {
        Self { client }
    }

    fn clean_jsonp(raw: &str) -> String {
        raw.replace(['(', ')', ';', '\n', '\r', '\t', ' '], "")
            .replace("topstock", "")
            .replace("{{", "{")
            .replace("}}}", "}}")
    }

    fn stock_from_value(code: &str, v: &Value) -> Stock {
        let name = as_string(v.get("name"));
        let now = as_f64(v.get("price"));
        let low = as_f64(v.get("low"));
        let high = as_f64(v.get("high"));
        let yesterday = as_f64(v.get("yestclose"));

        Stock {
            code: code.to_uppercase(),
            name,
            now,
            low,
            high,
            yesterday,
            percent: percent(now, yesterday),
        }
    }
}

#[async_trait]
impl StockApi for NeteaseApi {
    async fn get_stock(&self, code: &str) -> Result<Stock> {
        let transformer = NeteaseCommonCodeTransform;
        let transformed = transformer.transform(code)?;
        let url = format!(
            "https://api.money.126.net/data/feed/{},money.api?callback=topstock",
            transformed
        );
        let text = self.client.get(url).send().await?.text().await?;
        let row = Self::clean_jsonp(&text);
        let root: Value = serde_json::from_str(&row)?;

        match root.get(&transformed) {
            Some(v) => Ok(Self::stock_from_value(code, v)),
            None => Ok(Stock::default_with_code(code)),
        }
    }

    async fn get_stocks(&self, codes: &[String]) -> Result<Vec<Stock>> {
        let codes = dedup_codes(codes);
        if codes.is_empty() {
            return Ok(Vec::new());
        }

        let transformer = NeteaseCommonCodeTransform;
        let transformed = transformer.transforms(&codes)?;

        let url = format!(
            "https://api.money.126.net/data/feed/{},money.api?callback=topstock",
            transformed.join(",")
        );
        let text = self.client.get(url).send().await?.text().await?;
        let row = Self::clean_jsonp(&text);
        let root: Value = serde_json::from_str(&row)?;

        Ok(codes
            .iter()
            .map(|code| {
                let transformed = transformer.transform(code).unwrap_or_default();
                root.get(&transformed)
                    .map(|v| Self::stock_from_value(code, v))
                    .unwrap_or_else(|| Stock::default_with_code(code))
            })
            .collect())
    }

    async fn search_stocks(&self, key: &str) -> Result<Vec<Stock>> {
        let url = format!(
            "https://quotes.money.163.com/stocksearch/json.do?type=&count=5&word={}&callback=topstock",
            urlencoding::encode(key)
        );
        let text = self.client.get(url).send().await?.text().await?;
        let row = Self::clean_jsonp(&text);
        let items: Vec<Value> = serde_json::from_str(&row)?;

        let search_transformer = NeteaseSearchCodeTransform;
        let codes: Vec<String> = items
            .iter()
            .map(|item| {
                let tag = item.get("tag").and_then(Value::as_str).unwrap_or_default();
                let tp = item.get("type").and_then(Value::as_str).unwrap_or_default();
                let symbol = item
                    .get("symbol")
                    .and_then(Value::as_str)
                    .unwrap_or_default();
                search_transformer.transform(tag, tp, symbol)
            })
            .filter(|s| !s.is_empty())
            .collect();

        self.get_stocks(&dedup_codes(&codes)).await
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::stocks::transforms::base::CommonCodeTransform;
    use crate::stocks::transforms::netease::NeteaseCommonCodeTransform;

    #[test]
    fn test_to_api_code() {
        let transformer = NeteaseCommonCodeTransform;
        assert_eq!(transformer.transform("SH510500").unwrap(), "0510500");
        assert_eq!(transformer.transform("SZ399001").unwrap(), "1399001");
        assert_eq!(transformer.transform("HKHSI").unwrap(), "hkHSI");
        assert_eq!(transformer.transform("USDJI").unwrap(), "US_DJI");
    }

    #[test]
    fn test_stock_from_value() {
        let v: Value = serde_json::json!({
            "name": "500ETF",
            "price": 7.224,
            "low": 7.085,
            "high": 7.28,
            "yestclose": 7.149
        });
        let s = NeteaseApi::stock_from_value("SH510500", &v);
        assert_eq!(s.code, "SH510500");
        assert_eq!(s.name, "500ETF");
        assert!((s.now - 7.224).abs() < 1e-12);
        assert!((s.low - 7.085).abs() < 1e-12);
        assert!((s.high - 7.28).abs() < 1e-12);
        assert!((s.yesterday - 7.149).abs() < 1e-12);
    }
}