1use crate::error::Error;
2use crate::model::{Request, Response};
3#[cfg(test)]
4use mockito;
5use reqwest::header::{HeaderValue, CONTENT_TYPE};
6use serde_json::json;
7
8#[cfg(not(test))]
9fn jqdata_url() -> String {
10 String::from("https://dataapi.joinquant.com/apis")
11}
12
13#[cfg(test)]
14fn jqdata_url() -> String {
15 mockito::server_url()
16}
17
18#[derive(Debug, Clone)]
19pub struct JqdataClient {
20 token: String,
21}
22
23fn get_token(mob: &str, pwd: &str, reuse: bool) -> Result<String, Error> {
25 let method = if reuse {
26 "get_current_token"
27 } else {
28 "get_token"
29 };
30 let token_req = json!({
31 "method": method,
32 "mob": mob,
33 "pwd": pwd,
34 });
35 let client = reqwest::blocking::Client::new();
36 let response = client
37 .post(&jqdata_url())
38 .header(CONTENT_TYPE, HeaderValue::from_static("application/json"))
39 .body(token_req.to_string())
40 .send()?;
41 let token: String = response.text()?;
42 if token.starts_with("error") {
43 return Err(Error::Server(token));
44 }
45 Ok(token)
46}
47
48impl JqdataClient {
49 pub fn with_credential(mob: &str, pwd: &str) -> Result<Self, Error> {
50 let token = get_token(mob, pwd, true)?;
51 Ok(JqdataClient { token })
52 }
53
54 pub fn with_token(token: &str) -> Result<Self, Error> {
55 Ok(JqdataClient {
56 token: token.to_string(),
57 })
58 }
59
60 pub fn execute<C: Request + Response>(&self, command: C) -> Result<C::Output, Error> {
61 let req_body = command.request(&self.token)?;
62 let client = reqwest::blocking::Client::new();
63 let response = client
64 .post(&jqdata_url())
65 .header(CONTENT_TYPE, HeaderValue::from_static("application/json"))
66 .body(req_body)
67 .send()?;
68 let output = command.response(response)?;
69 Ok(output)
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76 use crate::model::*;
77 use mockito::mock;
78
79 #[test]
80 fn test_error_response() {
81 let response_body = "error: invalid token";
82 let _m = mock("POST", "/")
83 .with_status(200)
84 .with_body(response_body)
85 .create();
86
87 let client = JqdataClient::with_token("abc").unwrap();
88 let response = client.execute(GetAllSecurities {
89 code: SecurityKind::Stock,
90 date: None,
91 });
92 assert!(response.is_err());
93 }
94
95 #[test]
96 fn test_get_all_securities() {
97 let response_body = {
98 let mut s = String::from("code,display_name,name,start_date,end_date,type\n");
99 s.push_str("000001.XSHE,平安银行,PAYH,1991-04-03,2200-01-01,stock\n");
100 s.push_str("000002.XSHE,万科A,WKA,1991-01-29,2200-01-01,stock\n");
101 s
102 };
103 let _m = mock("POST", "/")
104 .with_status(200)
105 .with_body(&response_body)
106 .create();
107
108 let client = JqdataClient::with_token("abc").unwrap();
109 let ss = client
110 .execute(GetAllSecurities {
111 code: SecurityKind::Stock,
112 date: None,
113 })
114 .unwrap();
115 assert_eq!(
116 vec![
117 Security {
118 code: "000001.XSHE".to_string(),
119 display_name: "平安银行".to_string(),
120 name: "PAYH".to_string(),
121 start_date: "1991-04-03".to_string(),
122 end_date: "2200-01-01".to_string(),
123 kind: SecurityKind::Stock,
124 parent: None,
125 },
126 Security {
127 code: "000002.XSHE".to_string(),
128 display_name: "万科A".to_string(),
129 name: "WKA".to_string(),
130 start_date: "1991-01-29".to_string(),
131 end_date: "2200-01-01".to_string(),
132 kind: SecurityKind::Stock,
133 parent: None,
134 }
135 ],
136 ss
137 );
138 }
139}