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