#
tokens: 11709/50000 10/10 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | 
```