Skip to main content

xls_rs/
api.rs

1//! REST API server mode
2//!
3//! Provides HTTP API endpoints for xls-rs operations using axum.
4//!
5//! # Example
6//!
7//! ```no_run
8//! use xls_rs::api::{ApiConfig, ApiServer};
9//!
10//! #[tokio::main]
11//! async fn main() -> anyhow::Result<()> {
12//!     let config = ApiConfig::default();
13//!     let server = ApiServer::new(config);
14//!     server.start().await?;
15//!     Ok(())
16//! }
17//! ```
18
19use anyhow::Result;
20use serde::{Deserialize, Serialize};
21
22#[cfg(feature = "api")]
23use axum::{
24    extract::{DefaultBodyLimit, Json},
25    http::StatusCode,
26    response::{IntoResponse, Response},
27    routing::post,
28    Router,
29};
30#[cfg(feature = "api")]
31use tower_http::{
32    cors::{Any, CorsLayer},
33    limit::RequestBodyLimitLayer,
34};
35
36#[cfg(feature = "api")]
37use anyhow::Context;
38
39/// API server configuration
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct ApiConfig {
42    pub host: String,
43    pub port: u16,
44    pub cors_enabled: bool,
45    pub max_request_size: usize,
46}
47
48impl Default for ApiConfig {
49    fn default() -> Self {
50        Self {
51            host: "127.0.0.1".to_string(),
52            port: 8080,
53            cors_enabled: true,
54            max_request_size: 10 * 1024 * 1024, // 10MB
55        }
56    }
57}
58
59/// API request types
60#[derive(Debug, Deserialize)]
61#[serde(tag = "operation")]
62pub enum ApiRequest {
63    Read {
64        input: String,
65        sheet: Option<String>,
66        range: Option<String>,
67    },
68    Write {
69        output: String,
70        data: Vec<Vec<String>>,
71        sheet: Option<String>,
72    },
73    Convert {
74        input: String,
75        output: String,
76        sheet: Option<String>,
77    },
78    Profile {
79        input: String,
80        sample_size: Option<usize>,
81    },
82    Validate {
83        input: String,
84        rules: String,
85    },
86    Filter {
87        input: String,
88        where_clause: String,
89    },
90    Sort {
91        input: String,
92        column: String,
93        ascending: bool,
94    },
95}
96
97/// API response
98#[derive(Debug, Serialize)]
99pub struct ApiResponse {
100    pub success: bool,
101    pub data: Option<serde_json::Value>,
102    pub error: Option<String>,
103    pub message: Option<String>,
104}
105
106impl ApiResponse {
107    pub fn success(data: serde_json::Value) -> Self {
108        Self {
109            success: true,
110            data: Some(data),
111            error: None,
112            message: None,
113        }
114    }
115
116    pub fn error(message: String) -> Self {
117        Self {
118            success: false,
119            data: None,
120            error: Some(message),
121            message: None,
122        }
123    }
124
125    pub fn message(message: String) -> Self {
126        Self {
127            success: true,
128            data: None,
129            error: None,
130            message: Some(message),
131        }
132    }
133}
134
135/// API server
136pub struct ApiServer {
137    config: ApiConfig,
138}
139
140impl ApiServer {
141    pub fn new(config: ApiConfig) -> Self {
142        Self { config }
143    }
144
145    /// Start the API server (requires the "api" feature)
146    #[cfg(feature = "api")]
147    pub async fn start(&self) -> Result<()> {
148        // Build our application with routes
149        let app = Router::new()
150            .route("/api/read", post(handle_read))
151            .route("/api/write", post(handle_write))
152            .route("/api/convert", post(handle_convert))
153            .route("/api/profile", post(handle_profile))
154            .route("/api/validate", post(handle_validate))
155            .route("/api/filter", post(handle_filter))
156            .route("/api/sort", post(handle_sort))
157            .layer(DefaultBodyLimit::max(self.config.max_request_size))
158            .layer(RequestBodyLimitLayer::new(self.config.max_request_size));
159
160        let app = if self.config.cors_enabled {
161            app.layer(CorsLayer::new().allow_origin(Any).allow_methods(Any).allow_headers(Any))
162        } else {
163            app
164        };
165
166        let addr = format!("{}:{}", self.config.host, self.config.port);
167        let listener = tokio::net::TcpListener::bind(&addr)
168            .await
169            .with_context(|| format!("Failed to bind to {addr}"))?;
170
171        println!("🚀 API server listening on http://{}", addr);
172        println!("📊 Available endpoints:");
173        println!("   POST /api/read      - Read data from a file");
174        println!("   POST /api/write     - Write data to a file");
175        println!("   POST /api/convert   - Convert between file formats");
176        println!("   POST /api/profile   - Generate data profile");
177        println!("   POST /api/validate  - Validate data against rules");
178        println!("   POST /api/filter    - Filter data rows");
179        println!("   POST /api/sort      - Sort data by column");
180
181        axum::serve(listener, app).await.context("API server error")?;
182
183        Ok(())
184    }
185
186    /// Start the API server (fallback when "api" feature is not enabled)
187    #[cfg(not(feature = "api"))]
188    pub async fn start(&self) -> Result<()> {
189        use anyhow::bail;
190        bail!(
191            "API server is not enabled. Please rebuild with the 'api' feature: cargo build --features api"
192        )
193    }
194}
195
196/// Error response type
197#[cfg(feature = "api")]
198struct ApiError(anyhow::Error);
199
200#[cfg(feature = "api")]
201impl IntoResponse for ApiError {
202    fn into_response(self) -> Response {
203        let body = Json(ApiResponse::error(self.0.to_string()));
204        (StatusCode::INTERNAL_SERVER_ERROR, body).into_response()
205    }
206}
207
208/// Handler for /api/read
209#[cfg(feature = "api")]
210async fn handle_read(Json(req): Json<ApiRequest>) -> Result<Json<ApiResponse>, ApiError> {
211    use crate::converter::Converter;
212    use crate::csv_handler::CellRange;
213    use crate::helpers::filter_by_range;
214
215    let converter = Converter::new();
216
217    let (input, sheet, range) = match req {
218        ApiRequest::Read { input, sheet, range } => (input, sheet, range),
219        _ => return Err(ApiError(anyhow::anyhow!("Invalid request"))),
220    };
221
222    let mut data = converter
223        .read_any_data(&input, sheet.as_deref())
224        .map_err(ApiError)?;
225
226    if let Some(ref range_str) = range {
227        let cell = CellRange::parse(range_str).map_err(ApiError)?;
228        data = filter_by_range(&data, &cell);
229    }
230
231    let response = ApiResponse::success(serde_json::json!({ "data": data }));
232
233    Ok(Json(response))
234}
235
236/// Handler for /api/write
237#[cfg(feature = "api")]
238async fn handle_write(Json(req): Json<ApiRequest>) -> Result<Json<ApiResponse>, ApiError> {
239    use crate::traits::{DataWriteOptions, DataWriter};
240
241    let (output, data, sheet) = match req {
242        ApiRequest::Write { output, data, sheet } => (output, data, sheet),
243        _ => return Err(ApiError(anyhow::anyhow!("Invalid request"))),
244    };
245
246    // Determine file format from extension
247    let format = output
248        .rsplit('.')
249        .next()
250        .ok_or_else(|| ApiError(anyhow::anyhow!("Invalid file path")))?;
251
252    let options = DataWriteOptions {
253        sheet_name: sheet,
254        column_names: None,
255        include_headers: true,
256    };
257
258    match format {
259        "csv" => {
260            use crate::csv_handler::CsvHandler;
261            let handler = CsvHandler::new();
262            handler
263                .write(&output, &data, options)
264                .map_err(ApiError)?;
265        }
266        "xlsx" => {
267            use crate::excel::ExcelHandler;
268            let handler = ExcelHandler::new();
269            handler
270                .write(&output, &data, options)
271                .map_err(ApiError)?;
272        }
273        "parquet" => {
274            use crate::columnar::ParquetHandler;
275            let handler = ParquetHandler::new();
276            let (col_names, body): (Option<&[String]>, &[Vec<String>]) =
277                if options.include_headers && !data.is_empty() {
278                    (Some(&data[0]), data.get(1..).unwrap_or_default())
279                } else {
280                    (None, &data)
281                };
282            if body.is_empty() {
283                return Err(ApiError(anyhow::anyhow!(
284                    "Cannot write empty data to Parquet"
285                )));
286            }
287            handler
288                .write(&output, body, col_names)
289                .map_err(ApiError)?;
290        }
291        "avro" => {
292            use crate::columnar::AvroHandler;
293            let handler = AvroHandler::new();
294            let (field_names, body): (Option<&[String]>, &[Vec<String>]) =
295                if options.include_headers && !data.is_empty() {
296                    (Some(&data[0]), data.get(1..).unwrap_or_default())
297                } else {
298                    (None, &data)
299                };
300            if body.is_empty() {
301                return Err(ApiError(anyhow::anyhow!("Cannot write empty data to Avro")));
302            }
303            handler
304                .write(&output, body, field_names)
305                .map_err(ApiError)?;
306        }
307        _ => {
308            return Err(ApiError(anyhow::anyhow!(
309                "Unsupported output format: {}",
310                format
311            )))
312        }
313    }
314
315    Ok(Json(ApiResponse::message(format!(
316        "Data written to {}",
317        output
318    ))))
319}
320
321/// Handler for /api/convert
322#[cfg(feature = "api")]
323async fn handle_convert(Json(req): Json<ApiRequest>) -> Result<Json<ApiResponse>, ApiError> {
324    use crate::converter::Converter;
325
326    let (input, output, sheet) = match req {
327        ApiRequest::Convert { input, output, sheet } => (input, output, sheet),
328        _ => return Err(ApiError(anyhow::anyhow!("Invalid request"))),
329    };
330
331    let converter = Converter::new();
332    converter
333        .convert(&input, &output, sheet.as_deref())
334        .map_err(ApiError)?;
335
336    Ok(Json(ApiResponse::message(format!(
337        "Converted {} to {}",
338        input, output
339    ))))
340}
341
342/// Handler for /api/profile
343#[cfg(feature = "api")]
344async fn handle_profile(Json(req): Json<ApiRequest>) -> Result<Json<ApiResponse>, ApiError> {
345    use crate::converter::Converter;
346    use crate::profiling::DataProfiler;
347
348    let (input, sample_size) = match req {
349        ApiRequest::Profile { input, sample_size } => (input, sample_size),
350        _ => return Err(ApiError(anyhow::anyhow!("Invalid request"))),
351    };
352
353    let converter = Converter::new();
354    let data = converter
355        .read_any_data(&input, None)
356        .map_err(ApiError)?;
357
358    let mut profiler = DataProfiler::new();
359    if let Some(size) = sample_size {
360        profiler = profiler.with_sample_size(size);
361    }
362
363    let profile = profiler.profile(&data, &input).map_err(ApiError)?;
364
365    let value = serde_json::to_value(profile).map_err(|e| {
366        ApiError(anyhow::anyhow!("Failed to serialize profile: {}", e))
367    })?;
368
369    Ok(Json(ApiResponse::success(value)))
370}
371
372/// Handler for /api/validate
373#[cfg(feature = "api")]
374async fn handle_validate(Json(req): Json<ApiRequest>) -> Result<Json<ApiResponse>, ApiError> {
375    use crate::converter::Converter;
376    use crate::validation::{DataValidator, ValidationConfig};
377
378    let (input, rules) = match req {
379        ApiRequest::Validate { input, rules } => (input, rules),
380        _ => return Err(ApiError(anyhow::anyhow!("Invalid request"))),
381    };
382
383    let converter = Converter::new();
384    let data = converter
385        .read_any_data(&input, None)
386        .map_err(ApiError)?;
387
388    let config: ValidationConfig = serde_json::from_str(&rules)
389        .map_err(|e| ApiError(anyhow::anyhow!("Invalid validation config JSON: {}", e)))?;
390
391    let validator = DataValidator::new(config);
392    let result = validator.validate(&data).map_err(ApiError)?;
393
394    let value = serde_json::to_value(result)
395        .map_err(|e| ApiError(anyhow::anyhow!("Failed to serialize validation result: {}", e)))?;
396
397    Ok(Json(ApiResponse::success(value)))
398}
399
400/// Handler for /api/filter
401#[cfg(feature = "api")]
402async fn handle_filter(Json(req): Json<ApiRequest>) -> Result<Json<ApiResponse>, ApiError> {
403    use crate::converter::Converter;
404    use crate::operations::DataOperations;
405
406    let (input, where_clause) = match req {
407        ApiRequest::Filter {
408            input,
409            where_clause,
410        } => (input, where_clause),
411        _ => return Err(ApiError(anyhow::anyhow!("Invalid request"))),
412    };
413
414    let converter = Converter::new();
415    let data = converter.read_any_data(&input, None).map_err(ApiError)?;
416
417    let ops = DataOperations::new();
418    let filtered = ops.query(&data, &where_clause).map_err(ApiError)?;
419
420    Ok(Json(ApiResponse::success(serde_json::json!({ "data": filtered }))))
421}
422
423/// Handler for /api/sort
424#[cfg(feature = "api")]
425async fn handle_sort(Json(req): Json<ApiRequest>) -> Result<Json<ApiResponse>, ApiError> {
426    use crate::converter::Converter;
427    use crate::operations::DataOperations;
428    use crate::traits::SortOperator;
429
430    let (input, column, ascending) = match req {
431        ApiRequest::Sort {
432            input,
433            column,
434            ascending,
435        } => (input, column, ascending),
436        _ => return Err(ApiError(anyhow::anyhow!("Invalid request"))),
437    };
438
439    let converter = Converter::new();
440    let mut data = converter.read_any_data(&input, None).map_err(ApiError)?;
441
442    let ops = DataOperations::new();
443
444    // Find column index by name
445    if data.is_empty() {
446        return Err(ApiError(anyhow::anyhow!("Data is empty")));
447    }
448
449    let column_idx = data[0]
450        .iter()
451        .position(|c| c == &column)
452        .ok_or_else(|| ApiError(anyhow::anyhow!("Column '{}' not found", column)))?;
453
454    ops.sort(&mut data, column_idx, ascending).map_err(ApiError)?;
455
456    Ok(Json(ApiResponse::success(serde_json::json!({ "data": data }))))
457}
458
459#[cfg(test)]
460mod tests {
461    use super::*;
462
463    #[test]
464    fn test_api_config_default() {
465        let config = ApiConfig::default();
466        assert_eq!(config.host, "127.0.0.1");
467        assert_eq!(config.port, 8080);
468        assert!(config.cors_enabled);
469        assert_eq!(config.max_request_size, 10 * 1024 * 1024);
470    }
471
472    #[test]
473    fn test_api_response_success() {
474        let response = ApiResponse::success(serde_json::json!({"test": "data"}));
475        assert!(response.success);
476        assert!(response.data.is_some());
477        assert!(response.error.is_none());
478    }
479
480    #[test]
481    fn test_api_response_error() {
482        let response = ApiResponse::error("Test error".to_string());
483        assert!(!response.success);
484        assert!(response.data.is_none());
485        assert!(response.error.is_some());
486    }
487
488    #[test]
489    fn test_api_response_message() {
490        let response = ApiResponse::message("Test message".to_string());
491        assert!(response.success);
492        assert!(response.message.is_some());
493        assert_eq!(response.message.unwrap(), "Test message");
494    }
495}