tushare_api/traits.rs
1//! Traits for Tushare API data conversion
2//!
3//! This module contains traits that define how to convert Tushare API response data
4//! into Rust structs. The main trait is `FromTushareData` which can be implemented
5//! manually or automatically using the derive macro from `tushare-derive`.
6
7use crate::error::TushareError;
8use crate::types::{TushareResponse, TushareEntityList};
9use serde_json::Value;
10
11/// Trait for converting individual JSON values to custom types
12///
13/// This trait allows users to define how to convert a single JSON value
14/// from Tushare API responses into their custom types. It's designed to
15/// work with the procedural macro system for automatic field conversion.
16///
17/// # Example
18///
19/// ```rust
20/// use tushare_api::{FromTushareValue, TushareError};
21/// use serde_json::Value;
22///
23/// // Custom type example
24/// #[derive(Debug, Clone, PartialEq)]
25/// struct CustomDecimal(f64);
26///
27/// impl std::str::FromStr for CustomDecimal {
28/// type Err = std::num::ParseFloatError;
29/// fn from_str(s: &str) -> Result<Self, Self::Err> {
30/// s.parse::<f64>().map(CustomDecimal)
31/// }
32/// }
33///
34/// impl FromTushareValue for CustomDecimal {
35/// fn from_tushare_value(value: &Value) -> Result<Self, TushareError> {
36/// match value {
37/// Value::String(s) => s.parse().map_err(|e| {
38/// TushareError::ParseError(format!("Failed to parse decimal: {}", e))
39/// }),
40/// Value::Number(n) => {
41/// if let Some(f) = n.as_f64() {
42/// Ok(CustomDecimal(f))
43/// } else {
44/// Err(TushareError::ParseError("Invalid number format".to_string()))
45/// }
46/// }
47/// _ => Err(TushareError::ParseError("Value is not a valid decimal".to_string()))
48/// }
49/// }
50/// }
51/// ```
52pub trait FromTushareValue: Sized {
53 /// Convert a JSON value to this type
54 ///
55 /// # Arguments
56 ///
57 /// * `value` - The JSON value to convert
58 fn from_tushare_value(value: &Value) -> Result<Self, TushareError>;
59}
60
61// Note: Basic Rust type implementations have been moved to src/basic_types.rs
62// This includes implementations for: String, f64, f32, i64, i32, i16, i8, u64, u32, u16, u8, usize, isize, bool
63
64/// Trait for converting optional JSON values to custom types
65///
66/// This trait handles the conversion of potentially null or missing JSON values
67/// to optional custom types. It's designed to work with the procedural macro
68/// system for automatic optional field conversion.
69///
70/// # Example
71///
72/// ```rust
73/// use tushare_api::{FromTushareValue, FromOptionalTushareValue, TushareError};
74/// use serde_json::Value;
75///
76/// // Custom type example (same as above)
77/// #[derive(Debug, Clone, PartialEq)]
78/// struct CustomDecimal(f64);
79///
80/// impl std::str::FromStr for CustomDecimal {
81/// type Err = std::num::ParseFloatError;
82/// fn from_str(s: &str) -> Result<Self, Self::Err> {
83/// s.parse::<f64>().map(CustomDecimal)
84/// }
85/// }
86///
87/// impl FromTushareValue for CustomDecimal {
88/// fn from_tushare_value(value: &Value) -> Result<Self, TushareError> {
89/// match value {
90/// Value::String(s) => s.parse().map_err(|e| {
91/// TushareError::ParseError(format!("Failed to parse decimal: {}", e))
92/// }),
93/// Value::Number(n) => {
94/// if let Some(f) = n.as_f64() {
95/// Ok(CustomDecimal(f))
96/// } else {
97/// Err(TushareError::ParseError("Invalid number format".to_string()))
98/// }
99/// }
100/// _ => Err(TushareError::ParseError("Value is not a valid decimal".to_string()))
101/// }
102/// }
103/// }
104///
105/// impl FromOptionalTushareValue for CustomDecimal {
106/// fn from_optional_tushare_value(value: &Value) -> Result<Option<Self>, TushareError> {
107/// if value.is_null() {
108/// Ok(None)
109/// } else {
110/// match value {
111/// Value::String(s) if s.is_empty() => Ok(None),
112/// _ => CustomDecimal::from_tushare_value(value).map(Some)
113/// }
114/// }
115/// }
116/// }
117/// ```
118pub trait FromOptionalTushareValue: Sized {
119 /// Convert an optional JSON value to this type
120 ///
121 /// # Arguments
122 ///
123 /// * `value` - The JSON value to convert (may be null)
124 fn from_optional_tushare_value(value: &Value) -> Result<Option<Self>, TushareError>;
125}
126
127// Note: Basic Rust type implementations for FromOptionalTushareValue have been moved to src/basic_types.rs
128
129/// Trait for converting Tushare API response data into Rust structs
130///
131/// This trait defines how to convert a single row of data from a Tushare API response
132/// into a Rust struct. It can be implemented manually or automatically using the
133/// `#[derive(FromTushareData)]` macro from the `tushare-derive` crate.
134///
135/// # Example
136///
137/// ```rust
138/// use tushare_api::traits::FromTushareData;
139/// use tushare_api::error::TushareError;
140/// use serde_json::Value;
141///
142/// struct Stock {
143/// ts_code: String,
144/// name: String,
145/// }
146///
147/// impl FromTushareData for Stock {
148/// fn from_row(fields: &[String], values: &[Value]) -> Result<Self, TushareError> {
149/// // Manual implementation
150/// let ts_code_idx = fields.iter().position(|f| f == "ts_code")
151/// .ok_or_else(|| TushareError::ParseError("Missing ts_code field".to_string()))?;
152/// let name_idx = fields.iter().position(|f| f == "name")
153/// .ok_or_else(|| TushareError::ParseError("Missing name field".to_string()))?;
154///
155/// Ok(Stock {
156/// ts_code: values[ts_code_idx].as_str()
157/// .ok_or_else(|| TushareError::ParseError("Invalid ts_code".to_string()))?
158/// .to_string(),
159/// name: values[name_idx].as_str()
160/// .ok_or_else(|| TushareError::ParseError("Invalid name".to_string()))?
161/// .to_string(),
162/// })
163/// }
164/// }
165/// ```
166///
167/// # Using the derive macro
168///
169/// For most use cases, you can use the derive macro instead of manual implementation:
170///
171/// ```rust
172/// use tushare_api::DeriveFromTushareData;
173///
174/// #[derive(Debug, Clone, DeriveFromTushareData)]
175/// pub struct Stock {
176/// ts_code: String,
177/// name: String,
178/// area: Option<String>,
179/// }
180/// ```
181pub trait FromTushareData: Sized {
182 /// Convert a single row of data to this type
183 ///
184 /// # Arguments
185 ///
186 /// * `fields` - Field names from the response
187 /// * `values` - Values for this row
188 fn from_row(fields: &[String], values: &[Value]) -> Result<Self, TushareError>;
189}
190
191/// Implementation of `TryFrom<TushareResponse>` for `TushareEntityList<T>`
192///
193/// This allows automatic conversion from API responses to typed entity lists.
194/// It extracts pagination metadata and converts each data row to the target type T.
195impl<T> TryFrom<TushareResponse> for TushareEntityList<T>
196where
197 T: FromTushareData,
198{
199 type Error = TushareError;
200
201 fn try_from(response: TushareResponse) -> Result<Self, Self::Error> {
202 let Some(data) = response.data else {
203 return Err(TushareError::ParseError("Missing data in response".to_string()));
204 };
205 let mut items = Vec::new();
206 // Convert each row to the target type
207 for row in &data.items {
208 let item = T::from_row(&data.fields, row)?;
209 items.push(item);
210 }
211
212 Ok(TushareEntityList::new(
213 items,
214 data.has_more,
215 data.count,
216 ))
217 }
218}
219
220/// Helper function for parsing values with custom date format (non-optional types)
221///
222/// This function is used by the procedural macro when a `date_format` attribute is specified.
223/// It attempts to parse the value using the custom format for supported chrono types.
224///
225/// # Arguments
226///
227/// * `value` - The JSON value to parse
228/// * `format` - The custom date format string (e.g., "%d/%m/%Y")
229///
230/// # Returns
231///
232/// Returns the parsed value of type T or an error if parsing fails.
233pub fn from_tushare_value_with_date_format<T>(
234 value: &serde_json::Value,
235 format: &str,
236) -> Result<T, crate::error::TushareError>
237where
238 T: FromTushareValueWithFormat,
239{
240 T::from_tushare_value_with_format(value, format)
241}
242
243/// Helper function for parsing optional values with custom date format
244///
245/// This function is used by the procedural macro when a `date_format` attribute is specified
246/// for optional fields. It handles null/empty values gracefully.
247///
248/// # Arguments
249///
250/// * `value` - The JSON value to parse (may be null)
251/// * `format` - The custom date format string (e.g., "%d/%m/%Y")
252///
253/// # Returns
254///
255/// Returns Some(parsed_value) for valid values, None for null/empty, or an error for invalid formats.
256pub fn from_optional_tushare_value_with_date_format<T>(
257 value: &serde_json::Value,
258 format: &str,
259) -> Result<Option<T>, crate::error::TushareError>
260where
261 T: FromTushareValueWithFormat,
262{
263 match value {
264 serde_json::Value::Null => Ok(None),
265 serde_json::Value::String(s) if s.is_empty() => Ok(None),
266 _ => Ok(Some(T::from_tushare_value_with_format(value, format)?)),
267 }
268}
269
270/// Trait for types that support custom date format parsing
271///
272/// This trait is implemented for chrono date/time types to enable
273/// custom format parsing through the `#[tushare(date_format = "...")]` attribute.
274pub trait FromTushareValueWithFormat: Sized {
275 /// Parse a value using a custom date format
276 ///
277 /// # Arguments
278 ///
279 /// * `value` - The JSON value to parse
280 /// * `format` - The custom date format string
281 ///
282 /// # Returns
283 ///
284 /// Returns the parsed value or an error if parsing fails.
285 fn from_tushare_value_with_format(
286 value: &serde_json::Value,
287 format: &str,
288 ) -> Result<Self, crate::error::TushareError>;
289}