# 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 | ```