# 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: -------------------------------------------------------------------------------- ``` TUSHARE_TOKEN=xxxxxxx ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` /target .env .idea/ *.json **/.Ds_Store ``` -------------------------------------------------------------------------------- /ts-model/src/lib.rs: -------------------------------------------------------------------------------- ```rust pub mod endpoint; pub mod model; pub use endpoint::*; pub use model::*; ``` -------------------------------------------------------------------------------- /ts-model/Cargo.toml: -------------------------------------------------------------------------------- ```toml [package] name = "ts-model" version = "0.1.0" edition = "2021" [dependencies] serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" reqwest = { version = "0.12.15", features = ["json"] } tokio = { version = "1.44.2", features = ["full"] } ts-derive.workspace = true dotenvy.workspace = true ``` -------------------------------------------------------------------------------- /ts-derive/Cargo.toml: -------------------------------------------------------------------------------- ```toml [package] name = "ts-derive" version = "0.1.0" edition = "2021" description = "Derive macros for Tushare API integration" license = "MIT" authors = ["Your Name <[email protected]>"] repository = "https://github.com/hanxuanliang/tsrs-mcp-server" documentation = "https://docs.rs/ts-derive" readme = "README.md" keywords = ["tushare", "api", "derive", "macro"] categories = ["api-bindings", "development-tools"] [lib] proc-macro = true [dependencies] serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" reqwest = { version = "0.12.15", features = ["json"] } tokio = { version = "1.44.2", features = ["full"] } dotenvy = "0.15.7" syn = { version = "2.0", features = ["full", "extra-traits"] } quote = "1.0" proc-macro2 = "1.0" darling = "0.20.3" ``` -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- ```toml [package] name = "tsrs-mcp-server" version = "0.1.0" edition = "2024" [workspace] members = ["ts-derive", "ts-model"] [dependencies] tokio = { workspace = true } ts-model = { workspace = true } ts-derive = { workspace = true } poem-mcpserver = { workspace = true } poem = { workspace = true } schemars = { workspace = true } serde = { workspace = true } dotenvy = { workspace = true } tracing = "0.1" tracing-subscriber = "0.3" clap = { version = "4.5.3", features = ["derive"] } [workspace.dependencies] reqwest = { version = "0.12.15", features = ["json"] } tokio = { version = "1.44.2", features = ["full"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "2.0" url = "2.4" tracing = "0.1" tracing-subscriber = "0.3" lazy_static = "1.4.0" dotenvy = "0.15.7" poem-mcpserver = { version = "0.2.1", features = ["poem", "streamable-http"] } poem = { version = "3.1.9", features = ["sse"] } schemars = "0.8.22" chrono = "0.4" futures = "0.3" ts-derive = { path = "./ts-derive" } ts-model = { path = "./ts-model" } ``` -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- ```yaml name: release on: push: tags: - "v*" jobs: build-and-upload: name: Build and upload runs-on: ${{ matrix.os }} strategy: matrix: include: - build: macos-x86_64 os: macos-latest target: x86_64-apple-darwin - build: macos-aarch64 os: macos-latest target: aarch64-apple-darwin - build: windows-aarch64 os: windows-latest target: aarch64-pc-windows-msvc - build: windows-x86_64 os: windows-latest target: x86_64-pc-windows-msvc steps: - name: Checkout uses: actions/checkout@v3 - name: Get the release version from tag shell: bash run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV - name: Install Rust uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.target }} - name: Build uses: actions-rs/cargo@v1 with: use-cross: true command: build args: --verbose --release --target ${{ matrix.target }} - name: Build archive shell: bash run: | # Replace with the name of your binary binary_name="tsrs-mcp-server" dirname="$binary_name-${{ env.VERSION }}-${{ matrix.target }}" mkdir "$dirname" if [ "${{ matrix.os }}" = "windows-latest" ]; then mv "target/${{ matrix.target }}/release/$binary_name.exe" "$dirname" else mv "target/${{ matrix.target }}/release/$binary_name" "$dirname" fi if [ "${{ matrix.os }}" = "windows-latest" ]; then 7z a "$dirname.zip" "$dirname" echo "ASSET=$dirname.zip" >> $GITHUB_ENV else tar -czf "$dirname.tar.gz" "$dirname" echo "ASSET=$dirname.tar.gz" >> $GITHUB_ENV fi - name: Release uses: softprops/action-gh-release@v2 with: files: | ${{ env.ASSET }} token: ${{ secrets.PAT_GITHUB_TOKEN }} ``` -------------------------------------------------------------------------------- /ts-model/src/endpoint.rs: -------------------------------------------------------------------------------- ```rust use serde::Serialize; use ts_derive::TsEndpoint; use crate::{ ConceptListItem, KplConceptConsItem, KplListItem, LimitCptListItem, LimitStepItem, StkMinsItem, ThsHotItem, ThsMoneyflowCptItem, ThsMoneyflowItem, }; #[derive(TsEndpoint, Debug, Serialize)] #[endpoint(api = "limit_step", desc = "获取每天连板个数晋级的股票", resp = LimitStepItem)] pub struct LimitStepReq { pub trade_date: String, pub start_date: String, pub end_date: String, pub nums: String, } #[derive(TsEndpoint, Debug, Serialize)] #[endpoint(api = "limit_step", desc = "获取每天连板个数晋级的股票", resp = LimitStepItem)] pub struct HisLimitStepReq { pub start_date: String, pub end_date: String, } #[derive(TsEndpoint, Debug, Serialize)] #[endpoint(api = "ths_hot", desc = "获取同花顺App热榜数据", resp = ThsHotItem)] pub struct ThsHotReq { pub trade_date: String, pub market: String, } #[derive(TsEndpoint, Debug, Serialize)] #[endpoint(api = "kpl_list", desc = "获取涨跌停板数据", resp = KplListItem)] pub struct KplListReq { pub tag: String, pub trade_date: String, } #[derive(TsEndpoint, Debug, Serialize)] #[endpoint(api = "limit_list_ths", desc = "涨跌停榜单(同花顺)", resp = KplListItem)] pub struct LimitListThs { pub tag: String, pub trade_date: String, } #[derive(TsEndpoint, Debug, Serialize)] #[endpoint(api = "kpl_concept", desc = "获取开盘啦概念题材列表", resp = ConceptListItem)] pub struct KplConceptReq { pub trade_date: String, } #[derive(TsEndpoint, Debug, Serialize)] #[endpoint(api = "kpl_concept_cons", desc = "获取开盘啦概念题材的成分股", resp = KplConceptConsItem)] pub struct KplConceptConsReq { pub trade_date: String, pub ts_code: String, } #[derive(TsEndpoint, Debug, Serialize)] #[endpoint(api = "limit_cpt_list", desc = "获取每天涨停股票最多最强的概念板块", resp = LimitCptListItem)] pub struct LimitCptListReq { pub trade_date: String, pub start_date: String, pub end_date: String, } #[derive(TsEndpoint, Debug, Serialize)] #[endpoint(api = "moneyflow_ths", desc = "获取同花顺个股资金流向数据", resp = ThsMoneyflowItem)] pub struct ThsMoneyflowReq { pub ts_code: String, pub trade_date: String, pub start_date: String, pub end_date: String, } #[derive(TsEndpoint, Debug, Serialize)] #[endpoint(api = "moneyflow_cnt_ths", desc = "获取同花顺概念板块每日资金流向", resp = ThsMoneyflowCptItem)] pub struct ThsMoneyflowCptReq { pub trade_date: String, pub start_date: String, pub end_date: String, } #[derive(TsEndpoint, Debug, Serialize)] #[endpoint(api = "stk_mins", desc = "获取A股分钟数据", resp = StkMinsItem)] pub struct StkMinsReq { pub ts_code: String, pub freq: String, pub start_date: Option<String>, pub end_date: Option<String>, } #[cfg(test)] mod tests { use crate::endpoint::*; #[tokio::test] async fn test() { let res = ThsHotReq { trade_date: "20250407".to_string(), market: "热股".to_string(), } .execute() .await .unwrap_or_default(); println!("res: {:?}", res); } #[tokio::test] async fn test2() { let res: Vec<KplListItem> = KplListReq { tag: "涨停".to_string(), trade_date: "20250407".to_string(), } .execute_typed() .await .unwrap_or_default(); println!("{:?}", res); } #[tokio::test] async fn test_his_limit_step() { let res = HisLimitStepReq { start_date: "20250407".to_string(), end_date: "20250409".to_string(), } .execute_typed() .await .unwrap_or_default(); for item in res { println!( "code: {:?} name: {:?}, status: {:?} trade_date: {:?}", item.ts_code, item.name, item.nums, item.trade_date ); } } } ``` -------------------------------------------------------------------------------- /ts-model/src/model.rs: -------------------------------------------------------------------------------- ```rust use serde::{Deserialize, Serialize}; use ts_derive::TsResponse; #[derive(TsResponse, Serialize, Deserialize, Debug, Default)] #[response(api = "kpl_list")] pub struct KplListItem { #[ts_field(0)] #[serde(default)] pub ts_code: String, #[ts_field(1)] #[serde(default)] pub name: String, #[ts_field(2)] #[serde(default)] pub trade_date: String, #[ts_field(3)] #[serde(default)] pub lu_time: String, #[ts_field(4)] #[serde(default)] pub ld_time: String, #[ts_field(5)] #[serde(default)] pub open_time: String, #[ts_field(6)] #[serde(default)] pub last_time: String, #[ts_field(7)] #[serde(default)] pub lu_desc: String, #[ts_field(8)] #[serde(default)] pub tag: String, #[ts_field(9)] #[serde(default)] pub theme: String, #[ts_field(10)] #[serde(default)] pub net_change: f64, #[ts_field(11)] #[serde(default)] pub bid_amount: f64, #[ts_field(12)] #[serde(default)] pub status: String, #[ts_field(13)] #[serde(default)] pub bid_change: f64, #[ts_field(14)] #[serde(default)] pub bid_turnover: f64, #[ts_field(15)] #[serde(default)] pub lu_bid_vol: f64, #[ts_field(16)] #[serde(default)] pub pct_chg: f64, #[ts_field(17)] #[serde(default)] pub bid_pct_chg: f64, #[ts_field(18)] #[serde(default)] pub rt_pct_chg: f64, #[ts_field(19)] #[serde(default)] pub limit_order: f64, #[ts_field(20)] #[serde(default)] pub amount: f64, #[ts_field(21)] #[serde(default)] pub turnover_rate: f64, #[ts_field(22)] #[serde(default)] pub free_float: f64, #[ts_field(23)] #[serde(default)] pub lu_limit_order: f64, } #[derive(TsResponse, Serialize, Debug)] #[response(api = "kpl_concept")] pub struct ConceptListItem { #[ts_field(0)] pub trade_date: String, #[ts_field(1)] pub ts_code: String, #[ts_field(2)] pub name: String, #[ts_field(3)] pub z_t_num: i64, #[ts_field(4)] pub up_num: String, } #[derive(TsResponse, Serialize, Debug)] #[response(api = "kpl_concept_cons")] pub struct KplConceptConsItem { #[ts_field(0)] pub ts_code: String, #[ts_field(1)] pub name: String, #[ts_field(2)] pub con_name: String, #[ts_field(3)] pub con_code: String, #[ts_field(4)] pub trade_date: String, #[ts_field(5)] pub desc: String, #[ts_field(6)] #[serde(default)] pub hot_num: String, } #[derive(TsResponse, Serialize, Deserialize, Debug)] #[response(api = "ths_hot")] pub struct ThsHotItem { #[ts_field(0)] pub trade_date: String, #[ts_field(1)] pub data_type: String, #[ts_field(2)] pub ts_code: String, #[ts_field(3)] pub ts_name: String, #[ts_field(4)] pub rank: i32, #[ts_field(5)] pub pct_change: f64, #[ts_field(6)] pub current_price: f64, #[ts_field(7)] pub concept: String, #[ts_field(8)] pub rank_reason: String, #[ts_field(9)] pub hot: f64, #[ts_field(10)] pub rank_time: String, } #[derive(TsResponse, Serialize, Debug)] #[response(api = "limit_step")] pub struct LimitStepItem { #[ts_field(0)] pub ts_code: String, #[ts_field(1)] pub name: String, #[ts_field(2)] pub trade_date: String, #[ts_field(3)] pub nums: String, } #[derive(TsResponse, Serialize, Debug)] #[response(api = "limit_cpt_list")] pub struct LimitCptListItem { #[ts_field(0)] pub ts_code: String, #[ts_field(1)] pub name: String, #[ts_field(2)] pub trade_date: String, #[ts_field(3)] pub days: i32, #[ts_field(4)] pub up_stat: String, #[ts_field(5)] pub cons_nums: i32, #[ts_field(6)] pub up_nums: i32, #[ts_field(7)] pub pct_chg: f64, #[ts_field(8)] pub rank: String, } #[derive(TsResponse, Serialize, Debug)] #[response(api = "moneyflow_ths")] pub struct ThsMoneyflowItem { #[ts_field(0)] pub trade_date: String, #[ts_field(1)] pub ts_code: String, #[ts_field(2)] pub name: String, #[ts_field(3)] pub pct_change: f64, #[ts_field(4)] pub latest: f64, #[ts_field(5)] pub net_amount: f64, #[ts_field(6)] pub net_d5_amount: f64, #[ts_field(7)] pub buy_lg_amount: f64, #[ts_field(8)] pub buy_lg_amount_rate: f64, #[ts_field(9)] pub buy_md_amount: f64, #[ts_field(10)] pub buy_md_amount_rate: f64, #[ts_field(11)] pub buy_sm_amount: f64, #[ts_field(12)] pub buy_sm_amount_rate: f64, } #[derive(TsResponse, Serialize, Debug)] #[response(api = "moneyflow_cnt_ths")] pub struct ThsMoneyflowCptItem { #[ts_field(0)] pub trade_date: String, #[ts_field(1)] pub ts_code: String, #[ts_field(2)] pub name: String, #[ts_field(3)] pub lead_stock: String, #[ts_field(4)] pub close_price: f64, #[ts_field(5)] pub pct_change: f64, #[ts_field(6)] pub index_close: f64, #[ts_field(7)] pub company_num: i32, #[ts_field(8)] pub pct_change_stock: f64, #[ts_field(9)] pub net_buy_amount: f64, #[ts_field(10)] pub net_sell_amount: f64, #[ts_field(11)] pub net_amount: f64, } #[derive(TsResponse, Serialize, Debug)] #[response(api = "stk_mins")] pub struct StkMinsItem { #[ts_field(0)] pub ts_code: String, #[ts_field(1)] pub trade_time: String, #[ts_field(2)] pub open: f64, #[ts_field(3)] pub close: f64, #[ts_field(4)] pub high: f64, #[ts_field(5)] pub low: f64, #[ts_field(6)] pub vol: i64, #[ts_field(7)] pub amount: f64, } ``` -------------------------------------------------------------------------------- /ts-derive/src/lib.rs: -------------------------------------------------------------------------------- ```rust #![allow(dead_code)] use darling::FromMeta; use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, Data, DeriveInput, Fields, LitInt, Type}; /// Options for the TsEndpoint derive macro #[derive(Debug, FromMeta)] struct EndpointOpts { /// API name for the Tushare request api: String, /// Description of the API endpoint desc: String, /// Response type (optional) #[darling(default)] resp: Option<syn::Path>, } /// Options for the TsResponse derive macro #[derive(Debug, FromMeta)] struct ResponseOpts { /// API name for the Tushare response api: String, } /// Derive macro for Tushare API endpoints /// /// Example usage: /// ```rust /// #[derive(TsEndpoint)] /// #[endpoint(api = "api_name", desc = "description", resp = MyResponseType)] /// struct MyRequest { /// // ... fields ... /// } /// ``` #[proc_macro_derive(TsEndpoint, attributes(endpoint, fields))] pub fn ts_endpoint_derive(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let name = &input.ident; let requester_name = syn::Ident::new(&format!("{}Requester", name), name.span()); // Parse endpoint options using darling let endpoint_opts = match input .attrs .iter() .find(|attr| attr.path().is_ident("endpoint")) .map(|attr| EndpointOpts::from_meta(&attr.meta)) .transpose() { Ok(Some(opts)) => opts, Ok(None) => { return syn::Error::new_spanned( input.ident.clone(), "Missing #[endpoint(...)] attribute", ) .to_compile_error() .into() } Err(e) => return TokenStream::from(e.write_errors()), }; // Extract fields for request parameters let fields = match &input.data { Data::Struct(data) => match &data.fields { Fields::Named(fields) => &fields.named, _ => { return syn::Error::new_spanned( input.ident.clone(), "TsEndpoint only supports structs with named fields", ) .to_compile_error() .into() } }, _ => { return syn::Error::new_spanned(input.ident.clone(), "TsEndpoint only supports structs") .to_compile_error() .into() } }; // Generate field serialization for the params object let param_fields = fields.iter().map(|field| { let field_name = field.ident.as_ref().unwrap(); let field_name_str = field_name.to_string(); // Check for serde rename attribute let mut rename_value = None; for attr in &field.attrs { if attr.path().is_ident("serde") { let _ = attr.parse_nested_meta(|meta| { if meta.path.is_ident("rename") { rename_value = Some(meta.value()?.parse::<syn::LitStr>()?.value()); } Ok(()) }); } } // Use rename value if present, otherwise use field name let param_name = rename_value.unwrap_or_else(|| field_name_str.clone()); quote! { params.insert(#param_name.to_string(), serde_json::to_value(&self.#field_name)?); } }); // Get API name and description from the endpoint options let api_name = &endpoint_opts.api; let api_desc = &endpoint_opts.desc; // Check if response type is specified let resp_type = endpoint_opts.resp.as_ref().map(|path| quote! { #path }); // Generate the TsRequesterImpl struct implementation with a unique name let ts_requester_impl = if let Some(resp_type) = resp_type.clone() { quote! { // 定义单独的TsRequester结构体和impl,这个结构体是在当前crate中的 pub struct #requester_name { request: #name, fields: Option<Vec<&'static str>>, } impl #requester_name { pub fn new(request: #name, fields: Option<Vec<&'static str>>) -> Self { Self { request, fields } } pub fn with_fields(mut self, fields: Vec<&'static str>) -> Self { self.fields = Some(fields); self } pub async fn execute(self) -> Result<serde_json::Value, Box<dyn std::error::Error>> { self.request.__execute_request(self.fields).await } pub async fn execute_typed(self) -> Result<Vec<#resp_type>, Box<dyn std::error::Error>> { // If fields are not provided, extract field names from the response struct let fields_to_use = if self.fields.is_none() { // Get field names from the response struct by reflection let field_names = <#resp_type>::get_field_names(); Some(field_names) } else { self.fields }; // Execute with the fields (either provided or derived) let json = self.request.__execute_request(fields_to_use).await?; let res = <#resp_type>::from_json(&json); res } pub async fn execute_as_dicts(self) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>, Box<dyn std::error::Error>> { use serde_json::Value; use std::collections::HashMap; // 直接使用__execute_request而不是execute,以便保留字段信息 let json = self.request.__execute_request(self.fields).await?; // Extract fields and items let data = json.get("data") .ok_or("Missing 'data' field in response")?; let fields = data.get("fields") .ok_or("Missing 'fields' field in data")? .as_array() .ok_or("'fields' is not an array")?; let items = data.get("items") .ok_or("Missing 'items' field in data")? .as_array() .ok_or("'items' is not an array")?; // Convert to Vec<HashMap<String, Value>> let mut result = Vec::with_capacity(items.len()); for item_value in items { let item = item_value.as_array() .ok_or("Item is not an array")?; let mut map = HashMap::new(); // Map fields to values for (i, field) in fields.iter().enumerate() { if i < item.len() { let field_name = field.as_str() .ok_or("Field name is not a string")? .to_string(); map.insert(field_name, item[i].clone()); } } result.push(map); } Ok(result) } } } } else { quote! { // 定义单独的TsRequester结构体和impl,这个结构体是在当前crate中的 pub struct #requester_name { request: #name, fields: Option<Vec<&'static str>>, } impl #requester_name { pub fn new(request: #name, fields: Option<Vec<&'static str>>) -> Self { Self { request, fields } } pub fn with_fields(mut self, fields: Vec<&'static str>) -> Self { self.fields = Some(fields); self } pub async fn execute(self) -> Result<serde_json::Value, Box<dyn std::error::Error>> { self.request.__execute_request(self.fields).await } pub async fn execute_as_dicts(self) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>, Box<dyn std::error::Error>> { use serde_json::Value; use std::collections::HashMap; // 直接使用__execute_request而不是execute,以便保留字段信息 let json = self.request.__execute_request(self.fields).await?; // Extract fields and items let data = json.get("data") .ok_or("Missing 'data' field in response")?; let fields = data.get("fields") .ok_or("Missing 'fields' field in data")? .as_array() .ok_or("'fields' is not an array")?; let items = data.get("items") .ok_or("Missing 'items' field in data")? .as_array() .ok_or("'items' is not an array")?; // Convert to Vec<HashMap<String, Value>> let mut result = Vec::with_capacity(items.len()); for item_value in items { let item = item_value.as_array() .ok_or("Item is not an array")?; let mut map = HashMap::new(); // Map fields to values for (i, field) in fields.iter().enumerate() { if i < item.len() { let field_name = field.as_str() .ok_or("Field name is not a string")? .to_string(); map.insert(field_name, item[i].clone()); } } result.push(map); } Ok(result) } } } }; // Generate impl for the struct let impl_struct = quote! { impl #name { /// Get the API name pub fn api_name(&self) -> &'static str { #api_name } /// Get the API description pub fn description(&self) -> &'static str { #api_desc } /// Start chain with fields pub fn with_fields(self, fields: Vec<&'static str>) -> #requester_name { #requester_name::new(self, Some(fields)) } /// Execute without fields pub async fn execute(self) -> Result<serde_json::Value, Box<dyn std::error::Error>> { self.__execute_request(None).await } /// Execute with typed response, automatically deriving fields from response struct pub async fn execute_typed(self) -> Result<Vec<#resp_type>, Box<dyn std::error::Error>> { // Create requester and call its execute_typed method let requester = #requester_name::new(self, None); requester.execute_typed().await } // Inner method used by TsRequester #[doc(hidden)] pub(crate) async fn __execute_request(&self, fields: Option<Vec<&str>>) -> Result<serde_json::Value, Box<dyn std::error::Error>> { use serde_json::{json, Map, Value}; use reqwest::Client; use dotenvy::dotenv; use std::env; // Load environment variables dotenv().ok(); // Get token from environment let token = env::var("TUSHARE_TOKEN") .map_err(|_| "TUSHARE_TOKEN environment variable not set")?; // Build params object let mut params = Map::new(); #(#param_fields)* // Create request body let mut request_body = Map::new(); request_body.insert("api_name".to_string(), Value::String(#api_name.to_string())); request_body.insert("token".to_string(), Value::String(token)); request_body.insert("params".to_string(), Value::Object(params)); // Add fields if provided if let Some(field_list) = fields { request_body.insert("fields".to_string(), Value::String(field_list.join(","))); } // Send request let client = Client::new(); let response = client .post("http://api.tushare.pro/") .header("Content-Type", "application/json") .body(serde_json::to_string(&Value::Object(request_body))?) .send() .await?; if !response.status().is_success() { return Err(format!("Request failed with status: {}", response.status()).into()); } let json = response.json::<Value>().await?; Ok(json) } } }; // Combine implementations let output = quote! { #impl_struct #ts_requester_impl }; output.into() } /// Derive macro for Tushare API response models /// /// This macro will create structs that represent the response data from a Tushare API call. /// It automatically maps the fields to the data items in the response. /// /// Example usage: /// ```rust /// #[derive(TsResponse)] /// #[response(api = "api_name")] /// struct MyResponseData { /// #[ts_field(0)] /// field_one: String, /// #[ts_field(1)] /// field_two: i64, /// // ... /// } /// ``` #[proc_macro_derive(TsResponse, attributes(response, ts_field))] pub fn ts_response_derive(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let name = &input.ident; // Parse response options let response_opts = match input .attrs .iter() .find(|attr| attr.path().is_ident("response")) .map(|attr| ResponseOpts::from_meta(&attr.meta)) .transpose() { Ok(Some(opts)) => opts, Ok(None) => { return syn::Error::new_spanned( input.ident.clone(), "Missing #[response(...)] attribute", ) .to_compile_error() .into() } Err(e) => return TokenStream::from(e.write_errors()), }; // Extract fields for response data let fields = match &input.data { Data::Struct(data) => match &data.fields { Fields::Named(fields) => &fields.named, _ => { return syn::Error::new_spanned( input.ident.clone(), "TsResponse only supports structs with named fields", ) .to_compile_error() .into() } }, _ => { return syn::Error::new_spanned(input.ident.clone(), "TsResponse only supports structs") .to_compile_error() .into() } }; // Generate field parsing for the response items let field_parsers = fields.iter().map(|field| { let field_name = field.ident.as_ref().unwrap(); let field_type = &field.ty; // Extract index from ts_field attribute let mut field_index = None; // Check for #[serde(default)] attribute let mut has_serde_default = false; for attr in &field.attrs { if attr.path().is_ident("ts_field") { match attr.meta.require_list() { Ok(nested) => { // Parse the first token in the list as a literal integer let lit: LitInt = match syn::parse2(nested.tokens.clone()) { Ok(lit) => lit, Err(e) => return e.to_compile_error(), }; field_index = Some(lit.base10_parse::<usize>().unwrap()); } Err(e) => return e.to_compile_error(), } } else if attr.path().is_ident("serde") { // Use parse_nested_meta for a more robust check let _ = attr.parse_nested_meta(|meta| { if meta.path.is_ident("default") { has_serde_default = true; } // Ignore other serde attributes like rename, skip, etc. // Need to handle potential errors within meta if attributes are complex Ok(()) }); // Ignoring potential parse_nested_meta error for simplicity for now } } let index = match field_index { Some(idx) => idx, None => { return syn::Error::new_spanned(field_name, "Missing #[ts_field(index)] attribute") .to_compile_error() } }; let from_value = if field_type_is_option(field_type) { // Logic for Option<T> quote! { let #field_name = if item.len() > #index { let val = &item[#index]; if val.is_null() { None } else { Some(serde_json::from_value(val.clone())?) } } else { None // Treat missing index as None for Option types }; } } else if has_serde_default { // Logic for non-Option<T> with #[serde(default)] quote! { let #field_name:#field_type = if item.len() > #index { let val = &item[#index]; if val.is_null() { Default::default() // Use default if null } else { // Using unwrap_or_default() on the Result is cleaner serde_json::from_value(val.clone()).unwrap_or_default() } } else { Default::default() // Use default if index out of bounds }; } } else { // Logic for non-Option<T> *without* #[serde(default)] quote! { let #field_name = if item.len() > #index { let val = &item[#index]; // Error on null for non-optional, non-default fields if val.is_null() { return Err(format!("Field '{}' at index {} is null, but type is not Option and #[serde(default)] is not specified", stringify!(#field_name), #index).into()); } serde_json::from_value(val.clone())? } else { return Err(format!("Field index {} out of bounds for required field '{}'", #index, stringify!(#field_name)).into()); }; } }; quote! { #from_value } }); // 生成字段名称列表(用于构造和获取字段名) let field_names: Vec<_> = fields .iter() .map(|field| field.ident.as_ref().unwrap().clone()) .collect(); // 生成用于构造结构体的字段列表 let struct_field_tokens = { let field_idents = &field_names; quote! { #(#field_idents),* } }; // Get API name let api_name = &response_opts.api; // Generate implementation for parsing response let output = quote! { impl #name { /// Parse a list of items from Tushare API response pub fn from_json(json: &serde_json::Value) -> Result<Vec<Self>, Box<dyn std::error::Error>> { use serde_json::Value; // Extract data from response let data = json.get("data") .ok_or_else(|| "Missing 'data' field in response")?; let items = data.get("items") .ok_or_else(|| "Missing 'items' field in data")? .as_array() .ok_or_else(|| "'items' is not an array")?; let mut result = Vec::with_capacity(items.len()); for item_value in items { let item = item_value.as_array() .ok_or_else(|| "Item is not an array")?; #(#field_parsers)* result.push(Self { #struct_field_tokens }); } Ok(result) } /// Get the API name for this response pub fn api_name() -> &'static str { #api_name } /// Get field names from the response struct pub fn get_field_names() -> Vec<&'static str> { vec![ #(stringify!(#field_names)),* ] } } // Implement From<Value> to allow automatic conversion from JSON impl From<serde_json::Value> for #name { fn from(value: serde_json::Value) -> Self { // This is just a placeholder implementation to satisfy the trait bound // The actual conversion is handled by the from_json method panic!("Direct conversion from Value to {} is not supported, use from_json instead", stringify!(#name)); } } }; output.into() } /// Check if a type is an Option<T> fn field_type_is_option(ty: &Type) -> bool { if let Type::Path(type_path) = ty { if let Some(segment) = type_path.path.segments.first() { return segment.ident == "Option"; } } false } ```