1use 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#[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, }
56 }
57}
58
59#[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#[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
135pub struct ApiServer {
137 config: ApiConfig,
138}
139
140impl ApiServer {
141 pub fn new(config: ApiConfig) -> Self {
142 Self { config }
143 }
144
145 #[cfg(feature = "api")]
147 pub async fn start(&self) -> Result<()> {
148 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 #[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#[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#[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#[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 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#[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#[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#[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#[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#[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 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}