# Directory Structure
```
├── .env.example
├── .github
│ └── workflows
│ └── release.yml
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── docs
│ └── chatwise.jpg
├── LICENSE
├── README.md
├── src
│ └── main.rs
├── ts-derive
│ ├── Cargo.toml
│ ├── README.md
│ └── src
│ └── lib.rs
└── ts-model
├── Cargo.toml
└── src
├── endpoint.rs
├── lib.rs
└── model.rs
```
# Files
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
```
1 | TUSHARE_TOKEN=xxxxxxx
2 |
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | /target
2 | .env
3 | .idea/
4 | *.json
5 | **/.Ds_Store
6 |
```
--------------------------------------------------------------------------------
/ts-model/src/lib.rs:
--------------------------------------------------------------------------------
```rust
1 | pub mod endpoint;
2 | pub mod model;
3 |
4 | pub use endpoint::*;
5 | pub use model::*;
6 |
```
--------------------------------------------------------------------------------
/ts-model/Cargo.toml:
--------------------------------------------------------------------------------
```toml
1 | [package]
2 | name = "ts-model"
3 | version = "0.1.0"
4 | edition = "2021"
5 |
6 | [dependencies]
7 | serde = { version = "1.0", features = ["derive"] }
8 | serde_json = "1.0"
9 | reqwest = { version = "0.12.15", features = ["json"] }
10 | tokio = { version = "1.44.2", features = ["full"] }
11 | ts-derive.workspace = true
12 | dotenvy.workspace = true
13 |
```
--------------------------------------------------------------------------------
/ts-derive/Cargo.toml:
--------------------------------------------------------------------------------
```toml
1 | [package]
2 | name = "ts-derive"
3 | version = "0.1.0"
4 | edition = "2021"
5 | description = "Derive macros for Tushare API integration"
6 | license = "MIT"
7 | authors = ["Your Name <[email protected]>"]
8 | repository = "https://github.com/hanxuanliang/tsrs-mcp-server"
9 | documentation = "https://docs.rs/ts-derive"
10 | readme = "README.md"
11 | keywords = ["tushare", "api", "derive", "macro"]
12 | categories = ["api-bindings", "development-tools"]
13 |
14 | [lib]
15 | proc-macro = true
16 |
17 | [dependencies]
18 | serde = { version = "1.0", features = ["derive"] }
19 | serde_json = "1.0"
20 | reqwest = { version = "0.12.15", features = ["json"] }
21 | tokio = { version = "1.44.2", features = ["full"] }
22 | dotenvy = "0.15.7"
23 | syn = { version = "2.0", features = ["full", "extra-traits"] }
24 | quote = "1.0"
25 | proc-macro2 = "1.0"
26 | darling = "0.20.3"
27 |
```
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
```toml
1 | [package]
2 | name = "tsrs-mcp-server"
3 | version = "0.1.0"
4 | edition = "2024"
5 |
6 | [workspace]
7 | members = ["ts-derive", "ts-model"]
8 |
9 | [dependencies]
10 | tokio = { workspace = true }
11 | ts-model = { workspace = true }
12 | ts-derive = { workspace = true }
13 | poem-mcpserver = { workspace = true }
14 | poem = { workspace = true }
15 | schemars = { workspace = true }
16 | serde = { workspace = true }
17 | dotenvy = { workspace = true }
18 |
19 | tracing = "0.1"
20 | tracing-subscriber = "0.3"
21 | clap = { version = "4.5.3", features = ["derive"] }
22 |
23 | [workspace.dependencies]
24 | reqwest = { version = "0.12.15", features = ["json"] }
25 | tokio = { version = "1.44.2", features = ["full"] }
26 | serde = { version = "1.0", features = ["derive"] }
27 | serde_json = "1.0"
28 | thiserror = "2.0"
29 | url = "2.4"
30 | tracing = "0.1"
31 | tracing-subscriber = "0.3"
32 | lazy_static = "1.4.0"
33 | dotenvy = "0.15.7"
34 | poem-mcpserver = { version = "0.2.1", features = ["poem", "streamable-http"] }
35 | poem = { version = "3.1.9", features = ["sse"] }
36 | schemars = "0.8.22"
37 | chrono = "0.4"
38 | futures = "0.3"
39 | ts-derive = { path = "./ts-derive" }
40 | ts-model = { path = "./ts-model" }
41 |
```
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: release
2 | on:
3 | push:
4 | tags:
5 | - "v*"
6 |
7 | jobs:
8 | build-and-upload:
9 | name: Build and upload
10 | runs-on: ${{ matrix.os }}
11 |
12 | strategy:
13 | matrix:
14 | include:
15 | - build: macos-x86_64
16 | os: macos-latest
17 | target: x86_64-apple-darwin
18 |
19 | - build: macos-aarch64
20 | os: macos-latest
21 | target: aarch64-apple-darwin
22 |
23 | - build: windows-aarch64
24 | os: windows-latest
25 | target: aarch64-pc-windows-msvc
26 |
27 | - build: windows-x86_64
28 | os: windows-latest
29 | target: x86_64-pc-windows-msvc
30 |
31 | steps:
32 | - name: Checkout
33 | uses: actions/checkout@v3
34 |
35 | - name: Get the release version from tag
36 | shell: bash
37 | run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
38 |
39 | - name: Install Rust
40 | uses: dtolnay/rust-toolchain@stable
41 | with:
42 | targets: ${{ matrix.target }}
43 |
44 | - name: Build
45 | uses: actions-rs/cargo@v1
46 | with:
47 | use-cross: true
48 | command: build
49 | args: --verbose --release --target ${{ matrix.target }}
50 |
51 | - name: Build archive
52 | shell: bash
53 | run: |
54 | # Replace with the name of your binary
55 | binary_name="tsrs-mcp-server"
56 |
57 | dirname="$binary_name-${{ env.VERSION }}-${{ matrix.target }}"
58 | mkdir "$dirname"
59 | if [ "${{ matrix.os }}" = "windows-latest" ]; then
60 | mv "target/${{ matrix.target }}/release/$binary_name.exe" "$dirname"
61 | else
62 | mv "target/${{ matrix.target }}/release/$binary_name" "$dirname"
63 | fi
64 |
65 | if [ "${{ matrix.os }}" = "windows-latest" ]; then
66 | 7z a "$dirname.zip" "$dirname"
67 | echo "ASSET=$dirname.zip" >> $GITHUB_ENV
68 | else
69 | tar -czf "$dirname.tar.gz" "$dirname"
70 | echo "ASSET=$dirname.tar.gz" >> $GITHUB_ENV
71 | fi
72 |
73 | - name: Release
74 | uses: softprops/action-gh-release@v2
75 | with:
76 | files: |
77 | ${{ env.ASSET }}
78 | token: ${{ secrets.PAT_GITHUB_TOKEN }}
79 |
```
--------------------------------------------------------------------------------
/ts-model/src/endpoint.rs:
--------------------------------------------------------------------------------
```rust
1 | use serde::Serialize;
2 | use ts_derive::TsEndpoint;
3 |
4 | use crate::{
5 | ConceptListItem, KplConceptConsItem, KplListItem, LimitCptListItem, LimitStepItem, StkMinsItem,
6 | ThsHotItem, ThsMoneyflowCptItem, ThsMoneyflowItem,
7 | };
8 |
9 | #[derive(TsEndpoint, Debug, Serialize)]
10 | #[endpoint(api = "limit_step", desc = "获取每天连板个数晋级的股票", resp = LimitStepItem)]
11 | pub struct LimitStepReq {
12 | pub trade_date: String,
13 | pub start_date: String,
14 | pub end_date: String,
15 | pub nums: String,
16 | }
17 |
18 | #[derive(TsEndpoint, Debug, Serialize)]
19 | #[endpoint(api = "limit_step", desc = "获取每天连板个数晋级的股票", resp = LimitStepItem)]
20 | pub struct HisLimitStepReq {
21 | pub start_date: String,
22 | pub end_date: String,
23 | }
24 |
25 | #[derive(TsEndpoint, Debug, Serialize)]
26 | #[endpoint(api = "ths_hot", desc = "获取同花顺App热榜数据", resp = ThsHotItem)]
27 | pub struct ThsHotReq {
28 | pub trade_date: String,
29 | pub market: String,
30 | }
31 |
32 | #[derive(TsEndpoint, Debug, Serialize)]
33 | #[endpoint(api = "kpl_list", desc = "获取涨跌停板数据", resp = KplListItem)]
34 | pub struct KplListReq {
35 | pub tag: String,
36 | pub trade_date: String,
37 | }
38 |
39 | #[derive(TsEndpoint, Debug, Serialize)]
40 | #[endpoint(api = "limit_list_ths", desc = "涨跌停榜单(同花顺)", resp = KplListItem)]
41 | pub struct LimitListThs {
42 | pub tag: String,
43 | pub trade_date: String,
44 | }
45 |
46 | #[derive(TsEndpoint, Debug, Serialize)]
47 | #[endpoint(api = "kpl_concept", desc = "获取开盘啦概念题材列表", resp = ConceptListItem)]
48 | pub struct KplConceptReq {
49 | pub trade_date: String,
50 | }
51 |
52 | #[derive(TsEndpoint, Debug, Serialize)]
53 | #[endpoint(api = "kpl_concept_cons", desc = "获取开盘啦概念题材的成分股", resp = KplConceptConsItem)]
54 | pub struct KplConceptConsReq {
55 | pub trade_date: String,
56 | pub ts_code: String,
57 | }
58 |
59 | #[derive(TsEndpoint, Debug, Serialize)]
60 | #[endpoint(api = "limit_cpt_list", desc = "获取每天涨停股票最多最强的概念板块", resp = LimitCptListItem)]
61 | pub struct LimitCptListReq {
62 | pub trade_date: String,
63 | pub start_date: String,
64 | pub end_date: String,
65 | }
66 |
67 | #[derive(TsEndpoint, Debug, Serialize)]
68 | #[endpoint(api = "moneyflow_ths", desc = "获取同花顺个股资金流向数据", resp = ThsMoneyflowItem)]
69 | pub struct ThsMoneyflowReq {
70 | pub ts_code: String,
71 | pub trade_date: String,
72 | pub start_date: String,
73 | pub end_date: String,
74 | }
75 |
76 | #[derive(TsEndpoint, Debug, Serialize)]
77 | #[endpoint(api = "moneyflow_cnt_ths", desc = "获取同花顺概念板块每日资金流向", resp = ThsMoneyflowCptItem)]
78 | pub struct ThsMoneyflowCptReq {
79 | pub trade_date: String,
80 | pub start_date: String,
81 | pub end_date: String,
82 | }
83 |
84 | #[derive(TsEndpoint, Debug, Serialize)]
85 | #[endpoint(api = "stk_mins", desc = "获取A股分钟数据", resp = StkMinsItem)]
86 | pub struct StkMinsReq {
87 | pub ts_code: String,
88 | pub freq: String,
89 | pub start_date: Option<String>,
90 | pub end_date: Option<String>,
91 | }
92 |
93 | #[cfg(test)]
94 | mod tests {
95 | use crate::endpoint::*;
96 |
97 | #[tokio::test]
98 | async fn test() {
99 | let res = ThsHotReq {
100 | trade_date: "20250407".to_string(),
101 | market: "热股".to_string(),
102 | }
103 | .execute()
104 | .await
105 | .unwrap_or_default();
106 |
107 | println!("res: {:?}", res);
108 | }
109 |
110 | #[tokio::test]
111 | async fn test2() {
112 | let res: Vec<KplListItem> = KplListReq {
113 | tag: "涨停".to_string(),
114 | trade_date: "20250407".to_string(),
115 | }
116 | .execute_typed()
117 | .await
118 | .unwrap_or_default();
119 |
120 | println!("{:?}", res);
121 | }
122 |
123 | #[tokio::test]
124 | async fn test_his_limit_step() {
125 | let res = HisLimitStepReq {
126 | start_date: "20250407".to_string(),
127 | end_date: "20250409".to_string(),
128 | }
129 | .execute_typed()
130 | .await
131 | .unwrap_or_default();
132 |
133 | for item in res {
134 | println!(
135 | "code: {:?} name: {:?}, status: {:?} trade_date: {:?}",
136 | item.ts_code, item.name, item.nums, item.trade_date
137 | );
138 | }
139 | }
140 | }
141 |
```
--------------------------------------------------------------------------------
/ts-model/src/model.rs:
--------------------------------------------------------------------------------
```rust
1 | use serde::{Deserialize, Serialize};
2 | use ts_derive::TsResponse;
3 |
4 | #[derive(TsResponse, Serialize, Deserialize, Debug, Default)]
5 | #[response(api = "kpl_list")]
6 | pub struct KplListItem {
7 | #[ts_field(0)]
8 | #[serde(default)]
9 | pub ts_code: String,
10 | #[ts_field(1)]
11 | #[serde(default)]
12 | pub name: String,
13 | #[ts_field(2)]
14 | #[serde(default)]
15 | pub trade_date: String,
16 | #[ts_field(3)]
17 | #[serde(default)]
18 | pub lu_time: String,
19 | #[ts_field(4)]
20 | #[serde(default)]
21 | pub ld_time: String,
22 | #[ts_field(5)]
23 | #[serde(default)]
24 | pub open_time: String,
25 | #[ts_field(6)]
26 | #[serde(default)]
27 | pub last_time: String,
28 | #[ts_field(7)]
29 | #[serde(default)]
30 | pub lu_desc: String,
31 | #[ts_field(8)]
32 | #[serde(default)]
33 | pub tag: String,
34 | #[ts_field(9)]
35 | #[serde(default)]
36 | pub theme: String,
37 | #[ts_field(10)]
38 | #[serde(default)]
39 | pub net_change: f64,
40 | #[ts_field(11)]
41 | #[serde(default)]
42 | pub bid_amount: f64,
43 | #[ts_field(12)]
44 | #[serde(default)]
45 | pub status: String,
46 | #[ts_field(13)]
47 | #[serde(default)]
48 | pub bid_change: f64,
49 | #[ts_field(14)]
50 | #[serde(default)]
51 | pub bid_turnover: f64,
52 | #[ts_field(15)]
53 | #[serde(default)]
54 | pub lu_bid_vol: f64,
55 | #[ts_field(16)]
56 | #[serde(default)]
57 | pub pct_chg: f64,
58 | #[ts_field(17)]
59 | #[serde(default)]
60 | pub bid_pct_chg: f64,
61 | #[ts_field(18)]
62 | #[serde(default)]
63 | pub rt_pct_chg: f64,
64 | #[ts_field(19)]
65 | #[serde(default)]
66 | pub limit_order: f64,
67 | #[ts_field(20)]
68 | #[serde(default)]
69 | pub amount: f64,
70 | #[ts_field(21)]
71 | #[serde(default)]
72 | pub turnover_rate: f64,
73 | #[ts_field(22)]
74 | #[serde(default)]
75 | pub free_float: f64,
76 | #[ts_field(23)]
77 | #[serde(default)]
78 | pub lu_limit_order: f64,
79 | }
80 |
81 | #[derive(TsResponse, Serialize, Debug)]
82 | #[response(api = "kpl_concept")]
83 | pub struct ConceptListItem {
84 | #[ts_field(0)]
85 | pub trade_date: String,
86 | #[ts_field(1)]
87 | pub ts_code: String,
88 | #[ts_field(2)]
89 | pub name: String,
90 | #[ts_field(3)]
91 | pub z_t_num: i64,
92 | #[ts_field(4)]
93 | pub up_num: String,
94 | }
95 |
96 | #[derive(TsResponse, Serialize, Debug)]
97 | #[response(api = "kpl_concept_cons")]
98 | pub struct KplConceptConsItem {
99 | #[ts_field(0)]
100 | pub ts_code: String,
101 | #[ts_field(1)]
102 | pub name: String,
103 | #[ts_field(2)]
104 | pub con_name: String,
105 | #[ts_field(3)]
106 | pub con_code: String,
107 | #[ts_field(4)]
108 | pub trade_date: String,
109 | #[ts_field(5)]
110 | pub desc: String,
111 | #[ts_field(6)]
112 | #[serde(default)]
113 | pub hot_num: String,
114 | }
115 |
116 | #[derive(TsResponse, Serialize, Deserialize, Debug)]
117 | #[response(api = "ths_hot")]
118 | pub struct ThsHotItem {
119 | #[ts_field(0)]
120 | pub trade_date: String,
121 | #[ts_field(1)]
122 | pub data_type: String,
123 | #[ts_field(2)]
124 | pub ts_code: String,
125 | #[ts_field(3)]
126 | pub ts_name: String,
127 | #[ts_field(4)]
128 | pub rank: i32,
129 | #[ts_field(5)]
130 | pub pct_change: f64,
131 | #[ts_field(6)]
132 | pub current_price: f64,
133 | #[ts_field(7)]
134 | pub concept: String,
135 | #[ts_field(8)]
136 | pub rank_reason: String,
137 | #[ts_field(9)]
138 | pub hot: f64,
139 | #[ts_field(10)]
140 | pub rank_time: String,
141 | }
142 |
143 | #[derive(TsResponse, Serialize, Debug)]
144 | #[response(api = "limit_step")]
145 | pub struct LimitStepItem {
146 | #[ts_field(0)]
147 | pub ts_code: String,
148 | #[ts_field(1)]
149 | pub name: String,
150 | #[ts_field(2)]
151 | pub trade_date: String,
152 | #[ts_field(3)]
153 | pub nums: String,
154 | }
155 |
156 | #[derive(TsResponse, Serialize, Debug)]
157 | #[response(api = "limit_cpt_list")]
158 | pub struct LimitCptListItem {
159 | #[ts_field(0)]
160 | pub ts_code: String,
161 | #[ts_field(1)]
162 | pub name: String,
163 | #[ts_field(2)]
164 | pub trade_date: String,
165 | #[ts_field(3)]
166 | pub days: i32,
167 | #[ts_field(4)]
168 | pub up_stat: String,
169 | #[ts_field(5)]
170 | pub cons_nums: i32,
171 | #[ts_field(6)]
172 | pub up_nums: i32,
173 | #[ts_field(7)]
174 | pub pct_chg: f64,
175 | #[ts_field(8)]
176 | pub rank: String,
177 | }
178 |
179 | #[derive(TsResponse, Serialize, Debug)]
180 | #[response(api = "moneyflow_ths")]
181 | pub struct ThsMoneyflowItem {
182 | #[ts_field(0)]
183 | pub trade_date: String,
184 | #[ts_field(1)]
185 | pub ts_code: String,
186 | #[ts_field(2)]
187 | pub name: String,
188 | #[ts_field(3)]
189 | pub pct_change: f64,
190 | #[ts_field(4)]
191 | pub latest: f64,
192 | #[ts_field(5)]
193 | pub net_amount: f64,
194 | #[ts_field(6)]
195 | pub net_d5_amount: f64,
196 | #[ts_field(7)]
197 | pub buy_lg_amount: f64,
198 | #[ts_field(8)]
199 | pub buy_lg_amount_rate: f64,
200 | #[ts_field(9)]
201 | pub buy_md_amount: f64,
202 | #[ts_field(10)]
203 | pub buy_md_amount_rate: f64,
204 | #[ts_field(11)]
205 | pub buy_sm_amount: f64,
206 | #[ts_field(12)]
207 | pub buy_sm_amount_rate: f64,
208 | }
209 |
210 | #[derive(TsResponse, Serialize, Debug)]
211 | #[response(api = "moneyflow_cnt_ths")]
212 | pub struct ThsMoneyflowCptItem {
213 | #[ts_field(0)]
214 | pub trade_date: String,
215 | #[ts_field(1)]
216 | pub ts_code: String,
217 | #[ts_field(2)]
218 | pub name: String,
219 | #[ts_field(3)]
220 | pub lead_stock: String,
221 | #[ts_field(4)]
222 | pub close_price: f64,
223 | #[ts_field(5)]
224 | pub pct_change: f64,
225 | #[ts_field(6)]
226 | pub index_close: f64,
227 | #[ts_field(7)]
228 | pub company_num: i32,
229 | #[ts_field(8)]
230 | pub pct_change_stock: f64,
231 | #[ts_field(9)]
232 | pub net_buy_amount: f64,
233 | #[ts_field(10)]
234 | pub net_sell_amount: f64,
235 | #[ts_field(11)]
236 | pub net_amount: f64,
237 | }
238 |
239 | #[derive(TsResponse, Serialize, Debug)]
240 | #[response(api = "stk_mins")]
241 | pub struct StkMinsItem {
242 | #[ts_field(0)]
243 | pub ts_code: String,
244 | #[ts_field(1)]
245 | pub trade_time: String,
246 | #[ts_field(2)]
247 | pub open: f64,
248 | #[ts_field(3)]
249 | pub close: f64,
250 | #[ts_field(4)]
251 | pub high: f64,
252 | #[ts_field(5)]
253 | pub low: f64,
254 | #[ts_field(6)]
255 | pub vol: i64,
256 | #[ts_field(7)]
257 | pub amount: f64,
258 | }
259 |
```
--------------------------------------------------------------------------------
/ts-derive/src/lib.rs:
--------------------------------------------------------------------------------
```rust
1 | #![allow(dead_code)]
2 |
3 | use darling::FromMeta;
4 | use proc_macro::TokenStream;
5 | use quote::quote;
6 | use syn::{parse_macro_input, Data, DeriveInput, Fields, LitInt, Type};
7 |
8 | /// Options for the TsEndpoint derive macro
9 | #[derive(Debug, FromMeta)]
10 | struct EndpointOpts {
11 | /// API name for the Tushare request
12 | api: String,
13 | /// Description of the API endpoint
14 | desc: String,
15 | /// Response type (optional)
16 | #[darling(default)]
17 | resp: Option<syn::Path>,
18 | }
19 |
20 | /// Options for the TsResponse derive macro
21 | #[derive(Debug, FromMeta)]
22 | struct ResponseOpts {
23 | /// API name for the Tushare response
24 | api: String,
25 | }
26 |
27 | /// Derive macro for Tushare API endpoints
28 | ///
29 | /// Example usage:
30 | /// ```rust
31 | /// #[derive(TsEndpoint)]
32 | /// #[endpoint(api = "api_name", desc = "description", resp = MyResponseType)]
33 | /// struct MyRequest {
34 | /// // ... fields ...
35 | /// }
36 | /// ```
37 | #[proc_macro_derive(TsEndpoint, attributes(endpoint, fields))]
38 | pub fn ts_endpoint_derive(input: TokenStream) -> TokenStream {
39 | let input = parse_macro_input!(input as DeriveInput);
40 | let name = &input.ident;
41 | let requester_name = syn::Ident::new(&format!("{}Requester", name), name.span());
42 |
43 | // Parse endpoint options using darling
44 | let endpoint_opts = match input
45 | .attrs
46 | .iter()
47 | .find(|attr| attr.path().is_ident("endpoint"))
48 | .map(|attr| EndpointOpts::from_meta(&attr.meta))
49 | .transpose()
50 | {
51 | Ok(Some(opts)) => opts,
52 | Ok(None) => {
53 | return syn::Error::new_spanned(
54 | input.ident.clone(),
55 | "Missing #[endpoint(...)] attribute",
56 | )
57 | .to_compile_error()
58 | .into()
59 | }
60 | Err(e) => return TokenStream::from(e.write_errors()),
61 | };
62 |
63 | // Extract fields for request parameters
64 | let fields = match &input.data {
65 | Data::Struct(data) => match &data.fields {
66 | Fields::Named(fields) => &fields.named,
67 | _ => {
68 | return syn::Error::new_spanned(
69 | input.ident.clone(),
70 | "TsEndpoint only supports structs with named fields",
71 | )
72 | .to_compile_error()
73 | .into()
74 | }
75 | },
76 | _ => {
77 | return syn::Error::new_spanned(input.ident.clone(), "TsEndpoint only supports structs")
78 | .to_compile_error()
79 | .into()
80 | }
81 | };
82 |
83 | // Generate field serialization for the params object
84 | let param_fields = fields.iter().map(|field| {
85 | let field_name = field.ident.as_ref().unwrap();
86 | let field_name_str = field_name.to_string();
87 |
88 | // Check for serde rename attribute
89 | let mut rename_value = None;
90 | for attr in &field.attrs {
91 | if attr.path().is_ident("serde") {
92 | let _ = attr.parse_nested_meta(|meta| {
93 | if meta.path.is_ident("rename") {
94 | rename_value = Some(meta.value()?.parse::<syn::LitStr>()?.value());
95 | }
96 | Ok(())
97 | });
98 | }
99 | }
100 |
101 | // Use rename value if present, otherwise use field name
102 | let param_name = rename_value.unwrap_or_else(|| field_name_str.clone());
103 |
104 | quote! {
105 | params.insert(#param_name.to_string(), serde_json::to_value(&self.#field_name)?);
106 | }
107 | });
108 |
109 | // Get API name and description from the endpoint options
110 | let api_name = &endpoint_opts.api;
111 | let api_desc = &endpoint_opts.desc;
112 |
113 | // Check if response type is specified
114 | let resp_type = endpoint_opts.resp.as_ref().map(|path| quote! { #path });
115 |
116 | // Generate the TsRequesterImpl struct implementation with a unique name
117 | let ts_requester_impl = if let Some(resp_type) = resp_type.clone() {
118 | quote! {
119 | // 定义单独的TsRequester结构体和impl,这个结构体是在当前crate中的
120 | pub struct #requester_name {
121 | request: #name,
122 | fields: Option<Vec<&'static str>>,
123 | }
124 |
125 | impl #requester_name {
126 | pub fn new(request: #name, fields: Option<Vec<&'static str>>) -> Self {
127 | Self { request, fields }
128 | }
129 |
130 | pub fn with_fields(mut self, fields: Vec<&'static str>) -> Self {
131 | self.fields = Some(fields);
132 | self
133 | }
134 |
135 | pub async fn execute(self) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
136 | self.request.__execute_request(self.fields).await
137 | }
138 |
139 | pub async fn execute_typed(self) -> Result<Vec<#resp_type>, Box<dyn std::error::Error>> {
140 | // If fields are not provided, extract field names from the response struct
141 | let fields_to_use = if self.fields.is_none() {
142 | // Get field names from the response struct by reflection
143 | let field_names = <#resp_type>::get_field_names();
144 | Some(field_names)
145 | } else {
146 | self.fields
147 | };
148 |
149 | // Execute with the fields (either provided or derived)
150 | let json = self.request.__execute_request(fields_to_use).await?;
151 | let res = <#resp_type>::from_json(&json);
152 | res
153 | }
154 |
155 | pub async fn execute_as_dicts(self) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>, Box<dyn std::error::Error>> {
156 | use serde_json::Value;
157 | use std::collections::HashMap;
158 |
159 | // 直接使用__execute_request而不是execute,以便保留字段信息
160 | let json = self.request.__execute_request(self.fields).await?;
161 |
162 | // Extract fields and items
163 | let data = json.get("data")
164 | .ok_or("Missing 'data' field in response")?;
165 |
166 | let fields = data.get("fields")
167 | .ok_or("Missing 'fields' field in data")?
168 | .as_array()
169 | .ok_or("'fields' is not an array")?;
170 |
171 | let items = data.get("items")
172 | .ok_or("Missing 'items' field in data")?
173 | .as_array()
174 | .ok_or("'items' is not an array")?;
175 |
176 | // Convert to Vec<HashMap<String, Value>>
177 | let mut result = Vec::with_capacity(items.len());
178 |
179 | for item_value in items {
180 | let item = item_value.as_array()
181 | .ok_or("Item is not an array")?;
182 |
183 | let mut map = HashMap::new();
184 |
185 | // Map fields to values
186 | for (i, field) in fields.iter().enumerate() {
187 | if i < item.len() {
188 | let field_name = field.as_str()
189 | .ok_or("Field name is not a string")?
190 | .to_string();
191 |
192 | map.insert(field_name, item[i].clone());
193 | }
194 | }
195 |
196 | result.push(map);
197 | }
198 |
199 | Ok(result)
200 | }
201 | }
202 | }
203 | } else {
204 | quote! {
205 | // 定义单独的TsRequester结构体和impl,这个结构体是在当前crate中的
206 | pub struct #requester_name {
207 | request: #name,
208 | fields: Option<Vec<&'static str>>,
209 | }
210 |
211 | impl #requester_name {
212 | pub fn new(request: #name, fields: Option<Vec<&'static str>>) -> Self {
213 | Self { request, fields }
214 | }
215 |
216 | pub fn with_fields(mut self, fields: Vec<&'static str>) -> Self {
217 | self.fields = Some(fields);
218 | self
219 | }
220 |
221 | pub async fn execute(self) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
222 | self.request.__execute_request(self.fields).await
223 | }
224 |
225 | pub async fn execute_as_dicts(self) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>, Box<dyn std::error::Error>> {
226 | use serde_json::Value;
227 | use std::collections::HashMap;
228 |
229 | // 直接使用__execute_request而不是execute,以便保留字段信息
230 | let json = self.request.__execute_request(self.fields).await?;
231 |
232 | // Extract fields and items
233 | let data = json.get("data")
234 | .ok_or("Missing 'data' field in response")?;
235 |
236 | let fields = data.get("fields")
237 | .ok_or("Missing 'fields' field in data")?
238 | .as_array()
239 | .ok_or("'fields' is not an array")?;
240 |
241 | let items = data.get("items")
242 | .ok_or("Missing 'items' field in data")?
243 | .as_array()
244 | .ok_or("'items' is not an array")?;
245 |
246 | // Convert to Vec<HashMap<String, Value>>
247 | let mut result = Vec::with_capacity(items.len());
248 |
249 | for item_value in items {
250 | let item = item_value.as_array()
251 | .ok_or("Item is not an array")?;
252 |
253 | let mut map = HashMap::new();
254 |
255 | // Map fields to values
256 | for (i, field) in fields.iter().enumerate() {
257 | if i < item.len() {
258 | let field_name = field.as_str()
259 | .ok_or("Field name is not a string")?
260 | .to_string();
261 |
262 | map.insert(field_name, item[i].clone());
263 | }
264 | }
265 |
266 | result.push(map);
267 | }
268 |
269 | Ok(result)
270 | }
271 | }
272 | }
273 | };
274 |
275 | // Generate impl for the struct
276 | let impl_struct = quote! {
277 | impl #name {
278 | /// Get the API name
279 | pub fn api_name(&self) -> &'static str {
280 | #api_name
281 | }
282 |
283 | /// Get the API description
284 | pub fn description(&self) -> &'static str {
285 | #api_desc
286 | }
287 |
288 | /// Start chain with fields
289 | pub fn with_fields(self, fields: Vec<&'static str>) -> #requester_name {
290 | #requester_name::new(self, Some(fields))
291 | }
292 |
293 | /// Execute without fields
294 | pub async fn execute(self) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
295 | self.__execute_request(None).await
296 | }
297 |
298 | /// Execute with typed response, automatically deriving fields from response struct
299 | pub async fn execute_typed(self) -> Result<Vec<#resp_type>, Box<dyn std::error::Error>> {
300 | // Create requester and call its execute_typed method
301 | let requester = #requester_name::new(self, None);
302 | requester.execute_typed().await
303 | }
304 |
305 | // Inner method used by TsRequester
306 | #[doc(hidden)]
307 | pub(crate) async fn __execute_request(&self, fields: Option<Vec<&str>>) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
308 | use serde_json::{json, Map, Value};
309 | use reqwest::Client;
310 | use dotenvy::dotenv;
311 | use std::env;
312 |
313 | // Load environment variables
314 | dotenv().ok();
315 |
316 | // Get token from environment
317 | let token = env::var("TUSHARE_TOKEN")
318 | .map_err(|_| "TUSHARE_TOKEN environment variable not set")?;
319 |
320 | // Build params object
321 | let mut params = Map::new();
322 | #(#param_fields)*
323 |
324 | // Create request body
325 | let mut request_body = Map::new();
326 | request_body.insert("api_name".to_string(), Value::String(#api_name.to_string()));
327 | request_body.insert("token".to_string(), Value::String(token));
328 | request_body.insert("params".to_string(), Value::Object(params));
329 |
330 | // Add fields if provided
331 | if let Some(field_list) = fields {
332 | request_body.insert("fields".to_string(),
333 | Value::String(field_list.join(",")));
334 | }
335 |
336 | // Send request
337 | let client = Client::new();
338 | let response = client
339 | .post("http://api.tushare.pro/")
340 | .header("Content-Type", "application/json")
341 | .body(serde_json::to_string(&Value::Object(request_body))?)
342 | .send()
343 | .await?;
344 |
345 | if !response.status().is_success() {
346 | return Err(format!("Request failed with status: {}", response.status()).into());
347 | }
348 |
349 | let json = response.json::<Value>().await?;
350 | Ok(json)
351 | }
352 | }
353 | };
354 |
355 | // Combine implementations
356 | let output = quote! {
357 | #impl_struct
358 | #ts_requester_impl
359 | };
360 |
361 | output.into()
362 | }
363 |
364 | /// Derive macro for Tushare API response models
365 | ///
366 | /// This macro will create structs that represent the response data from a Tushare API call.
367 | /// It automatically maps the fields to the data items in the response.
368 | ///
369 | /// Example usage:
370 | /// ```rust
371 | /// #[derive(TsResponse)]
372 | /// #[response(api = "api_name")]
373 | /// struct MyResponseData {
374 | /// #[ts_field(0)]
375 | /// field_one: String,
376 | /// #[ts_field(1)]
377 | /// field_two: i64,
378 | /// // ...
379 | /// }
380 | /// ```
381 | #[proc_macro_derive(TsResponse, attributes(response, ts_field))]
382 | pub fn ts_response_derive(input: TokenStream) -> TokenStream {
383 | let input = parse_macro_input!(input as DeriveInput);
384 | let name = &input.ident;
385 |
386 | // Parse response options
387 | let response_opts = match input
388 | .attrs
389 | .iter()
390 | .find(|attr| attr.path().is_ident("response"))
391 | .map(|attr| ResponseOpts::from_meta(&attr.meta))
392 | .transpose()
393 | {
394 | Ok(Some(opts)) => opts,
395 | Ok(None) => {
396 | return syn::Error::new_spanned(
397 | input.ident.clone(),
398 | "Missing #[response(...)] attribute",
399 | )
400 | .to_compile_error()
401 | .into()
402 | }
403 | Err(e) => return TokenStream::from(e.write_errors()),
404 | };
405 |
406 | // Extract fields for response data
407 | let fields = match &input.data {
408 | Data::Struct(data) => match &data.fields {
409 | Fields::Named(fields) => &fields.named,
410 | _ => {
411 | return syn::Error::new_spanned(
412 | input.ident.clone(),
413 | "TsResponse only supports structs with named fields",
414 | )
415 | .to_compile_error()
416 | .into()
417 | }
418 | },
419 | _ => {
420 | return syn::Error::new_spanned(input.ident.clone(), "TsResponse only supports structs")
421 | .to_compile_error()
422 | .into()
423 | }
424 | };
425 |
426 | // Generate field parsing for the response items
427 | let field_parsers = fields.iter().map(|field| {
428 | let field_name = field.ident.as_ref().unwrap();
429 | let field_type = &field.ty;
430 |
431 | // Extract index from ts_field attribute
432 | let mut field_index = None;
433 | // Check for #[serde(default)] attribute
434 | let mut has_serde_default = false;
435 |
436 | for attr in &field.attrs {
437 | if attr.path().is_ident("ts_field") {
438 | match attr.meta.require_list() {
439 | Ok(nested) => {
440 | // Parse the first token in the list as a literal integer
441 | let lit: LitInt = match syn::parse2(nested.tokens.clone()) {
442 | Ok(lit) => lit,
443 | Err(e) => return e.to_compile_error(),
444 | };
445 | field_index = Some(lit.base10_parse::<usize>().unwrap());
446 | }
447 | Err(e) => return e.to_compile_error(),
448 | }
449 | } else if attr.path().is_ident("serde") {
450 | // Use parse_nested_meta for a more robust check
451 | let _ = attr.parse_nested_meta(|meta| {
452 | if meta.path.is_ident("default") {
453 | has_serde_default = true;
454 | }
455 | // Ignore other serde attributes like rename, skip, etc.
456 | // Need to handle potential errors within meta if attributes are complex
457 | Ok(())
458 | });
459 | // Ignoring potential parse_nested_meta error for simplicity for now
460 | }
461 | }
462 |
463 | let index = match field_index {
464 | Some(idx) => idx,
465 | None => {
466 | return syn::Error::new_spanned(field_name, "Missing #[ts_field(index)] attribute")
467 | .to_compile_error()
468 | }
469 | };
470 |
471 | let from_value = if field_type_is_option(field_type) {
472 | // Logic for Option<T>
473 | quote! {
474 | let #field_name = if item.len() > #index {
475 | let val = &item[#index];
476 | if val.is_null() {
477 | None
478 | } else {
479 | Some(serde_json::from_value(val.clone())?)
480 | }
481 | } else {
482 | None // Treat missing index as None for Option types
483 | };
484 | }
485 | } else if has_serde_default {
486 | // Logic for non-Option<T> with #[serde(default)]
487 | quote! {
488 | let #field_name:#field_type = if item.len() > #index {
489 | let val = &item[#index];
490 | if val.is_null() {
491 | Default::default() // Use default if null
492 | } else {
493 | // Using unwrap_or_default() on the Result is cleaner
494 | serde_json::from_value(val.clone()).unwrap_or_default()
495 | }
496 | } else {
497 | Default::default() // Use default if index out of bounds
498 | };
499 | }
500 | } else {
501 | // Logic for non-Option<T> *without* #[serde(default)]
502 | quote! {
503 | let #field_name = if item.len() > #index {
504 | let val = &item[#index];
505 | // Error on null for non-optional, non-default fields
506 | if val.is_null() {
507 | return Err(format!("Field '{}' at index {} is null, but type is not Option and #[serde(default)] is not specified", stringify!(#field_name), #index).into());
508 | }
509 | serde_json::from_value(val.clone())?
510 | } else {
511 | return Err(format!("Field index {} out of bounds for required field '{}'", #index, stringify!(#field_name)).into());
512 | };
513 | }
514 | };
515 |
516 | quote! { #from_value }
517 | });
518 |
519 | // 生成字段名称列表(用于构造和获取字段名)
520 | let field_names: Vec<_> = fields
521 | .iter()
522 | .map(|field| field.ident.as_ref().unwrap().clone())
523 | .collect();
524 |
525 | // 生成用于构造结构体的字段列表
526 | let struct_field_tokens = {
527 | let field_idents = &field_names;
528 | quote! {
529 | #(#field_idents),*
530 | }
531 | };
532 |
533 | // Get API name
534 | let api_name = &response_opts.api;
535 |
536 | // Generate implementation for parsing response
537 | let output = quote! {
538 | impl #name {
539 | /// Parse a list of items from Tushare API response
540 | pub fn from_json(json: &serde_json::Value) -> Result<Vec<Self>, Box<dyn std::error::Error>> {
541 | use serde_json::Value;
542 |
543 | // Extract data from response
544 | let data = json.get("data")
545 | .ok_or_else(|| "Missing 'data' field in response")?;
546 |
547 | let items = data.get("items")
548 | .ok_or_else(|| "Missing 'items' field in data")?
549 | .as_array()
550 | .ok_or_else(|| "'items' is not an array")?;
551 |
552 | let mut result = Vec::with_capacity(items.len());
553 |
554 | for item_value in items {
555 | let item = item_value.as_array()
556 | .ok_or_else(|| "Item is not an array")?;
557 |
558 | #(#field_parsers)*
559 |
560 | result.push(Self {
561 | #struct_field_tokens
562 | });
563 | }
564 |
565 | Ok(result)
566 | }
567 |
568 | /// Get the API name for this response
569 | pub fn api_name() -> &'static str {
570 | #api_name
571 | }
572 |
573 | /// Get field names from the response struct
574 | pub fn get_field_names() -> Vec<&'static str> {
575 | vec![
576 | #(stringify!(#field_names)),*
577 | ]
578 | }
579 | }
580 |
581 | // Implement From<Value> to allow automatic conversion from JSON
582 | impl From<serde_json::Value> for #name {
583 | fn from(value: serde_json::Value) -> Self {
584 | // This is just a placeholder implementation to satisfy the trait bound
585 | // The actual conversion is handled by the from_json method
586 | panic!("Direct conversion from Value to {} is not supported, use from_json instead", stringify!(#name));
587 | }
588 | }
589 | };
590 |
591 | output.into()
592 | }
593 |
594 | /// Check if a type is an Option<T>
595 | fn field_type_is_option(ty: &Type) -> bool {
596 | if let Type::Path(type_path) = ty {
597 | if let Some(segment) = type_path.path.segments.first() {
598 | return segment.ident == "Option";
599 | }
600 | }
601 | false
602 | }
603 |
```