use anyhow::Result;
use colored::*;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
#[cfg(test)]
use crate::core::parser::ParameterLocation;
use crate::core::parser::UnifiedParameter;
#[derive(Debug, Clone, PartialEq)]
pub enum ParameterType {
Filter,
Sort,
Pagination,
Other,
}
pub fn detect_parameter_type(param: &UnifiedParameter) -> ParameterType {
let name_lower = param.name.to_lowercase();
if name_lower.contains("filter") || name_lower.contains("query") || name_lower.contains("where")
{
ParameterType::Filter
} else if name_lower.contains("sort") || name_lower.contains("order") {
ParameterType::Sort
} else if name_lower.contains("page")
|| name_lower.contains("limit")
|| name_lower.contains("offset")
{
ParameterType::Pagination
} else {
ParameterType::Other
}
}
pub fn generate_parameter_example(param: &UnifiedParameter) -> Value {
let param_type = detect_parameter_type(param);
let name_lower = param.name.to_lowercase();
match param_type {
ParameterType::Filter => {
json!("status = 'active' and created_date >= '2024-01-01'")
}
ParameterType::Sort => {
if name_lower.contains("sort") {
json!("created_date:desc,name:asc")
} else {
json!("created_date desc")
}
}
ParameterType::Pagination => {
if name_lower.contains("page") && !name_lower.contains("size") {
json!(1)
} else if name_lower.contains("size") || name_lower.contains("limit") {
json!(20)
} else if name_lower.contains("offset") {
json!(0)
} else {
json!(1)
}
}
ParameterType::Other => {
crate::core::examples::generate_smart_example(¶m.name, ¶m.schema)
}
}
}
pub struct QueryBuilder {
operation: String,
filters: Vec<String>,
filter_operator: String,
sorts: Vec<(String, String)>,
page: Option<i32>,
page_size: Option<i32>,
other_params: HashMap<String, String>,
}
impl QueryBuilder {
pub fn new(operation: String) -> Self {
Self {
operation,
filters: Vec::new(),
filter_operator: "and".to_string(),
sorts: Vec::new(),
page: None,
page_size: None,
other_params: HashMap::new(),
}
}
pub fn interactive_build(&mut self) -> Result<()> {
use std::io::{self, Write};
println!("\n{}", "🔨 QUERY BUILDER MODE".bright_cyan().bold());
println!("{}", "━".repeat(50).dimmed());
println!("\n{}", "Step 1: Build filter conditions".bright_yellow());
println!("Add filters one at a time (press Enter to skip):");
loop {
println!("\n{}", "Add a filter condition:".dimmed());
print!(" Field name (or press Enter to finish): ");
io::stdout().flush()?;
let mut field = String::new();
io::stdin().read_line(&mut field)?;
let field = field.trim();
if field.is_empty() {
break;
}
println!(" Available operators:");
println!(" {} - equals", "=".bright_green());
println!(" {} - not equals", "!=".bright_green());
println!(" {} - contains multiple values", "in".bright_green());
println!(
" {} - pattern match (use % for wildcard)",
"like".bright_green()
);
println!(" {} - greater than", ">".bright_green());
println!(" {} - less than", "<".bright_green());
println!(" {} - greater or equal", ">=".bright_green());
println!(" {} - less or equal", "<=".bright_green());
print!(" Choose operator: ");
io::stdout().flush()?;
let mut operator = String::new();
io::stdin().read_line(&mut operator)?;
let operator = operator.trim();
let value_prompt = if operator == "in" || operator == "not in" {
" Enter value(s) (comma-separated): "
} else {
" Enter value: "
};
print!("{}", value_prompt);
io::stdout().flush()?;
let mut value = String::new();
io::stdin().read_line(&mut value)?;
let value = value.trim();
let filter = if operator == "in" || operator == "not in" {
let values: Vec<String> = value
.split(',')
.map(|v| format!("\"{}\"", v.trim()))
.collect();
format!("{} {} ({})", field, operator, values.join(","))
} else if operator == "like" {
format!("{} {} \"{}\"", field, operator, value)
} else if operator == "=" || operator == "!=" {
if value.parse::<f64>().is_ok() {
format!("{} {} {}", field, operator, value)
} else {
format!("{} {} \"{}\"", field, operator, value)
}
} else {
format!("{} {} {}", field, operator, value)
};
self.filters.push(filter.clone());
println!(" ✓ Added: {}", filter.green());
}
if self.filters.len() > 1 {
print!("\n Combine filters with (and/or) [and]: ");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let input = input.trim();
if !input.is_empty() && (input == "or" || input == "and") {
self.filter_operator = input.to_string();
}
}
println!("\n{}", "Step 2: Add sort fields".bright_cyan());
println!("Enter sort fields (or press Enter to skip):");
loop {
print!(" Field name: ");
io::stdout().flush()?;
let mut field = String::new();
io::stdin().read_line(&mut field)?;
let field = field.trim();
if field.is_empty() {
break;
}
print!(" Direction (asc/desc) [asc]: ");
io::stdout().flush()?;
let mut direction = String::new();
io::stdin().read_line(&mut direction)?;
let direction = direction.trim();
let direction = if direction.is_empty() {
"asc"
} else {
direction
};
self.sorts.push((field.to_string(), direction.to_string()));
println!(" ✓ Added: {}:{}", field.green(), direction.green());
}
println!("\n{}", "Step 3: Pagination".bright_green());
print!(" Page number [1]: ");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let input = input.trim();
if !input.is_empty() {
self.page = input.parse().ok();
}
print!(" Page size [20]: ");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let input = input.trim();
if !input.is_empty() {
self.page_size = input.parse().ok();
}
println!("\n{}", "Generated command:".bright_green().bold());
println!("{}", self.generate_command().bright_cyan());
Ok(())
}
pub fn generate_command(&self) -> String {
let mut cmd = format!("mrapids run {}", self.operation);
if !self.filters.is_empty() {
let filter = self.filters.join(&format!(" {} ", self.filter_operator));
cmd.push_str(&format!(" \\\n --param 'filter={}'", filter));
}
if !self.sorts.is_empty() {
let sort = self
.sorts
.iter()
.map(|(f, d)| format!("{}:{}", f, d))
.collect::<Vec<_>>()
.join(",");
cmd.push_str(&format!(" \\\n --param 'sort={}'", sort));
}
if let Some(page) = self.page {
cmd.push_str(&format!(" \\\n --param page={}", page));
}
if let Some(page_size) = self.page_size {
cmd.push_str(&format!(" \\\n --param pageSize={}", page_size));
}
for (key, value) in &self.other_params {
cmd.push_str(&format!(" \\\n --param {}='{}'", key, value));
}
cmd
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct QueryHistory {
pub operation: String,
pub parameters: HashMap<String, String>,
pub timestamp: String,
pub response_time_ms: Option<u64>,
pub status_code: Option<u16>,
pub success: bool,
}
impl QueryHistory {
pub fn save(&self) -> Result<()> {
let history_dir = PathBuf::from(".mrapids/history");
fs::create_dir_all(&history_dir)?;
let filename = format!(
"{}-{}.json",
self.operation,
chrono::Utc::now().format("%Y%m%d-%H%M%S")
);
let path = history_dir.join(filename);
let json = serde_json::to_string_pretty(self)?;
fs::write(path, json)?;
Ok(())
}
}
pub fn show_query_help() {
println!("{}", "QUERY SYNTAX HELP".bright_cyan().bold());
println!("{}", "━".repeat(50).dimmed());
println!("\n{}", "SQL-like Filter Syntax:".bright_yellow());
println!(" Basic comparison:");
println!(" field = 'value'");
println!(" field != 'value'");
println!(" field > 100");
println!(" field >= '2024-01-01'");
println!("\n IN operator:");
println!(" field in ('value1', 'value2', 'value3')");
println!(" field not in ('excluded1', 'excluded2')");
println!("\n Pattern matching:");
println!(" field like 'prefix%'");
println!(" field like '%suffix'");
println!(" field like '%contains%'");
println!("\n Logical operators:");
println!(" condition1 and condition2");
println!(" condition1 or condition2");
println!("\n{}", "Sorting:".bright_cyan());
println!(" field:asc");
println!(" field:desc");
println!(" field1:asc,field2:desc");
println!("\n{}", "Pagination:".bright_green());
println!(" page=1");
println!(" pageSize=50");
}
pub fn save_query_to_history(
operation: String,
parameters: HashMap<String, String>,
response_time_ms: Option<u64>,
status_code: Option<u16>,
success: bool,
) -> Result<()> {
let history = QueryHistory {
operation,
parameters,
timestamp: chrono::Utc::now().to_rfc3339(),
response_time_ms,
status_code,
success,
};
history.save()?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_parameter_type() {
let filter_param = UnifiedParameter {
name: "filter".to_string(),
location: ParameterLocation::Query,
..Default::default()
};
assert_eq!(detect_parameter_type(&filter_param), ParameterType::Filter);
let sort_param = UnifiedParameter {
name: "orderBy".to_string(),
location: ParameterLocation::Query,
..Default::default()
};
assert_eq!(detect_parameter_type(&sort_param), ParameterType::Sort);
}
#[test]
fn test_query_builder_command_generation() {
let mut builder = QueryBuilder::new("testOp".to_string());
builder.filters.push("status = \"active\"".to_string());
builder.filters.push("price > 100".to_string());
builder.filter_operator = "and".to_string();
builder.sorts.push(("date".to_string(), "desc".to_string()));
builder.page = Some(1);
builder.page_size = Some(50);
let cmd = builder.generate_command();
assert!(cmd.contains("mrapids run testOp"));
assert!(cmd.contains("filter=status = \"active\" and price > 100"));
assert!(cmd.contains("sort=date:desc"));
assert!(cmd.contains("page=1"));
assert!(cmd.contains("pageSize=50"));
}
}
#[cfg(test)]
impl Default for UnifiedParameter {
fn default() -> Self {
use crate::core::parser::UnifiedSchema;
Self {
name: String::new(),
location: ParameterLocation::Query,
required: false,
schema: UnifiedSchema::default(),
description: None,
example: None,
}
}
}