kiteconnect_async_wasm/connect/
utils.rs

1//! # Utility Functions
2//!
3//! This module contains utility functions and platform-specific implementations
4//! used throughout the KiteConnect library. It provides cross-platform abstractions
5//! for HTTP requests, CSV parsing, and other common operations.
6//!
7//! ## Platform Support
8//!
9//! The utilities in this module are designed to work across different platforms:
10//! - **Native**: Full functionality with optimized implementations
11//! - **WASM**: Browser-compatible implementations using Web APIs
12//!
13//! ## Key Features
14//!
15//! - **Cross-platform HTTP handling**: Abstract interface for HTTP requests
16//! - **CSV parsing**: Platform-specific CSV parsing (native: `csv`, WASM: `csv-core`)
17//! - **URL management**: Centralized API endpoint configuration
18//! - **Error handling**: Consistent error patterns across platforms
19//!
20//! ## Example
21//!
22//! ```rust,no_run
23//! // CSV parsing is handled internally by the KiteConnect client
24//! use kiteconnect_async_wasm::connect::KiteConnect;
25//!
26//! # #[tokio::main]
27//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
28//! let client = KiteConnect::new("api_key", "access_token");
29//! // CSV parsing happens automatically when fetching instruments
30//! let instruments = client.instruments(None).await?;
31//! println!("Parsed {} instruments", instruments.as_array().unwrap().len());
32//! # Ok(())
33//! # }
34//! ```
35
36use anyhow::Result;
37#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
38use serde_json::Value as JsonValue;
39use std::collections::HashMap;
40
41// WASM platform imports
42#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
43use web_sys::window;
44
45#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
46use js_sys::Uint8Array;
47
48#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
49use wasm_bindgen_futures::JsFuture;
50
51#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
52use csv_core::{ReadFieldResult, Reader};
53
54/// Base URL for KiteConnect API in production
55#[cfg(not(test))]
56pub const URL: &str = "https://api.kite.trade";
57
58/// Base URL for KiteConnect API in test environment
59///
60/// Used during testing to point to a local mock server for reliable,
61/// offline testing without making actual API calls.
62#[cfg(test)]
63pub const URL: &str = "http://127.0.0.1:1234";
64
65/// Async trait for handling HTTP requests across different platforms
66///
67/// This trait provides a platform-agnostic interface for making HTTP requests.
68/// Implementations handle the specifics of each platform (native vs WASM)
69/// while providing a consistent API for the rest of the library.
70///
71/// # Platform Implementations
72///
73/// - **Native**: Uses `reqwest` for full HTTP client functionality
74/// - **WASM**: Uses `fetch` API for browser-compatible requests
75///
76/// # Example
77///
78/// ```rust,no_run
79/// use kiteconnect_async_wasm::connect::utils::RequestHandler;
80/// use std::collections::HashMap;
81///
82/// # struct MyClient;
83/// # impl RequestHandler for MyClient {
84/// #     async fn send_request(
85/// #         &self,
86/// #         url: reqwest::Url,
87/// #         method: &str,
88/// #         data: Option<HashMap<&str, &str>>,
89/// #     ) -> anyhow::Result<reqwest::Response> {
90/// #         unimplemented!()
91/// #     }
92/// # }
93///
94/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
95/// let client = MyClient;
96/// let url = reqwest::Url::parse("https://api.example.com/data")?;
97/// let mut params = HashMap::new();
98/// params.insert("key", "value");
99///
100/// let response = client.send_request(url, "GET", Some(params)).await?;
101/// # Ok(())
102/// # }
103/// ```
104pub trait RequestHandler {
105    /// Send an HTTP request with the specified parameters
106    ///
107    /// # Arguments
108    ///
109    /// * `url` - The complete URL to send the request to
110    /// * `method` - HTTP method ("GET", "POST", "PUT", "DELETE")
111    /// * `data` - Optional form data to include in the request
112    ///
113    /// # Returns
114    ///
115    /// A `Result` containing the HTTP response or an error
116    ///
117    /// # Errors
118    ///
119    /// Returns an error if:
120    /// - The network request fails
121    /// - The URL is malformed
122    /// - Authentication is required but missing
123    /// - The server returns an error status
124    fn send_request(
125        &self,
126        url: reqwest::Url,
127        method: &str,
128        data: Option<HashMap<&str, &str>>,
129    ) -> impl std::future::Future<Output = Result<reqwest::Response>> + Send;
130}
131
132/// Parse CSV data using csv-core for WASM compatibility
133///
134/// This function provides CSV parsing capability in WASM environments where
135/// the standard `csv` crate is not available. It uses `csv-core` which is
136/// a no-std implementation suitable for WebAssembly.
137///
138/// # Arguments
139///
140/// * `csv_data` - Raw CSV data as a string
141///
142/// # Returns
143///
144/// A `Result` containing the parsed CSV data as a JSON array of objects,
145/// where each object represents a row with column headers as keys.
146///
147/// # Errors
148///
149/// Returns an error if:
150/// - The CSV data is malformed
151/// - Memory allocation fails during parsing
152/// - JSON serialization fails
153///
154/// # Example
155///
156/// ```rust,no_run
157/// # #[cfg(all(feature = "wasm", target_arch = "wasm32"))]
158/// use kiteconnect_async_wasm::connect::utils::parse_csv_with_core;
159///
160/// # #[cfg(all(feature = "wasm", target_arch = "wasm32"))]
161/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
162/// let csv_data = r#"symbol,exchange,price
163/// RELIANCE,NSE,2500.00
164/// TCS,NSE,3200.50"#;
165///
166/// let parsed = parse_csv_with_core(csv_data)?;
167///
168/// // parsed is a JSON array:
169/// // [
170/// //   {"symbol": "RELIANCE", "exchange": "NSE", "price": "2500.00"},
171/// //   {"symbol": "TCS", "exchange": "NSE", "price": "3200.50"}
172/// // ]
173/// # Ok(())
174/// # }
175/// ```
176///
177/// # Platform Availability
178///
179/// This function is only available on WASM targets. On native platforms,
180/// use the standard `csv` crate which provides better performance and features.
181#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
182pub fn parse_csv_with_core(csv_data: &str) -> Result<JsonValue> {
183    let mut reader = Reader::new();
184    let mut output = vec![0; 1024];
185    let mut field = Vec::new();
186    let mut input = csv_data.as_bytes();
187
188    let mut headers: Vec<String> = Vec::new();
189    let mut records: Vec<Vec<String>> = Vec::new();
190    let mut current_record: Vec<String> = Vec::new();
191    let mut is_first_row = true;
192
193    loop {
194        let (result, input_consumed, output_written) = reader.read_field(input, &mut output);
195        input = &input[input_consumed..];
196
197        match result {
198            ReadFieldResult::InputEmpty => {
199                if !current_record.is_empty() {
200                    if is_first_row {
201                        headers = current_record.clone();
202                        is_first_row = false;
203                    } else {
204                        records.push(current_record.clone());
205                    }
206                }
207                break;
208            }
209            ReadFieldResult::OutputFull => {
210                field.extend_from_slice(&output[..output_written]);
211                // Continue reading with same input
212            }
213            ReadFieldResult::Field { record_end } => {
214                field.extend_from_slice(&output[..output_written]);
215                let field_str = String::from_utf8_lossy(&field).to_string();
216                current_record.push(field_str);
217                field.clear();
218
219                if record_end {
220                    if is_first_row {
221                        headers = current_record.clone();
222                        is_first_row = false;
223                    } else {
224                        records.push(current_record.clone());
225                    }
226                    current_record.clear();
227                }
228            }
229            ReadFieldResult::Record => {
230                // This case should not happen based on the API, but we handle it for completeness
231                continue;
232            }
233        }
234    }
235
236    // Convert to JSON format
237    let mut result: Vec<JsonValue> = Vec::new();
238    for record in records {
239        let mut obj = serde_json::Map::new();
240        for (i, value) in record.iter().enumerate() {
241            if let Some(header) = headers.get(i) {
242                obj.insert(header.clone(), JsonValue::String(value.clone()));
243            }
244        }
245        result.push(JsonValue::Object(obj));
246    }
247
248    Ok(JsonValue::Array(result))
249}