#![allow(dead_code)]
use crate::core::api::ApiError;
use anyhow::Result;
use colored::*;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SavedQuery {
pub name: String,
pub operation: String,
pub params: HashMap<String, String>,
#[serde(default)]
pub headers: HashMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub body: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub env: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_at: Option<String>,
}
impl SavedQuery {
pub fn from_run_params(
name: &str,
operation: &str,
params: &[String],
headers: &[String],
body: Option<&str>,
env: Option<&str>,
) -> Self {
let mut param_map = HashMap::new();
for param in params {
if let Some((key, value)) = param.split_once('=') {
param_map.insert(key.to_string(), value.to_string());
}
}
let mut header_map = HashMap::new();
for header in headers {
if let Some((key, value)) = header.split_once(':') {
header_map.insert(key.trim().to_string(), value.trim().to_string());
}
}
Self {
name: name.to_string(),
operation: operation.to_string(),
params: param_map,
headers: header_map,
body: body.map(String::from),
description: None,
env: env.map(String::from),
created_at: Some(chrono::Utc::now().to_rfc3339()),
}
}
pub fn to_args(&self) -> Vec<String> {
let mut args = vec!["run".to_string(), self.operation.clone()];
for (key, value) in &self.params {
args.push("--param".to_string());
args.push(format!("{}={}", key, value));
}
for (key, value) in &self.headers {
args.push("--header".to_string());
args.push(format!("{}: {}", key, value));
}
if let Some(body) = &self.body {
args.push("--data".to_string());
args.push(body.clone());
}
if let Some(env) = &self.env {
args.push("--env".to_string());
args.push(env.clone());
}
args
}
}
fn get_saved_queries_dir() -> Result<PathBuf> {
let dir = PathBuf::from(".mrapids/saved-queries");
if !dir.exists() {
fs::create_dir_all(&dir)?;
}
Ok(dir)
}
pub fn save_query(query: &SavedQuery) -> Result<PathBuf> {
let dir = get_saved_queries_dir()?;
let file_path = dir.join(format!("{}.yaml", query.name));
let yaml = serde_yaml::to_string(query)?;
fs::write(&file_path, yaml)?;
Ok(file_path)
}
pub fn load_query(name: &str) -> Result<SavedQuery> {
let dir = get_saved_queries_dir()?;
let file_path = dir.join(format!("{}.yaml", name));
if !file_path.exists() {
return Err(ApiError::ValidationError(format!("Saved query '{}' not found", name)).into());
}
let content = fs::read_to_string(&file_path)?;
let query: SavedQuery = serde_yaml::from_str(&content)?;
Ok(query)
}
pub fn list_saved_queries() -> Result<Vec<SavedQuery>> {
let dir = get_saved_queries_dir()?;
let mut queries = Vec::new();
if dir.exists() {
for entry in fs::read_dir(&dir)? {
let entry = entry?;
let path = entry.path();
if path
.extension()
.map_or(false, |ext| ext == "yaml" || ext == "yml")
{
if let Ok(content) = fs::read_to_string(&path) {
if let Ok(query) = serde_yaml::from_str::<SavedQuery>(&content) {
queries.push(query);
}
}
}
}
}
queries.sort_by(|a, b| a.name.cmp(&b.name));
Ok(queries)
}
pub fn delete_query(name: &str) -> Result<()> {
let dir = get_saved_queries_dir()?;
let file_path = dir.join(format!("{}.yaml", name));
if !file_path.exists() {
return Err(ApiError::ValidationError(format!("Saved query '{}' not found", name)).into());
}
fs::remove_file(&file_path)?;
Ok(())
}
pub fn display_saved_queries() -> Result<()> {
let queries = list_saved_queries()?;
if queries.is_empty() {
println!();
println!("{}", "No saved queries found.".dimmed());
println!();
println!("{}", "Save a query with:".bright_blue());
println!(" mrapids run <operation> -p key=value --save-query <name>");
println!();
return Ok(());
}
println!();
println!("{}", "Saved Queries:".bright_yellow());
println!();
println!(
" {:<20} {:<30} {}",
"Name".bright_white(),
"Operation".bright_white(),
"Parameters".bright_white()
);
println!(" {}", "─".repeat(70).dimmed());
for query in &queries {
let params_preview: String = query
.params
.iter()
.take(3)
.map(|(k, v)| format!("{}={}", k, truncate(v, 10)))
.collect::<Vec<_>>()
.join(", ");
let params_display = if query.params.len() > 3 {
format!("{} ...", params_preview)
} else if params_preview.is_empty() {
"(no params)".dimmed().to_string()
} else {
params_preview
};
println!(
" {:<20} {:<30} {}",
query.name.bright_cyan(),
query.operation,
params_display.dimmed()
);
}
println!();
println!(
"{} mrapids run --load-query <name>",
"Load with:".bright_blue()
);
println!();
Ok(())
}
pub fn display_query_details(name: &str) -> Result<()> {
let query = load_query(name)?;
println!();
println!("┌─────────────────────────────────────────────────────────────────┐");
println!("│ Saved Query: {:<50} │", query.name.bright_cyan());
println!("└─────────────────────────────────────────────────────────────────┘");
println!();
println!(" {} {}", "Operation:".bright_white(), query.operation);
if !query.params.is_empty() {
println!();
println!(" {}", "Parameters:".bright_white());
for (key, value) in &query.params {
println!(" {} = {}", key.bright_cyan(), value);
}
}
if !query.headers.is_empty() {
println!();
println!(" {}", "Headers:".bright_white());
for (key, value) in &query.headers {
println!(" {}: {}", key.bright_cyan(), value);
}
}
if let Some(body) = &query.body {
println!();
println!(" {}", "Body:".bright_white());
println!(" {}", body.dimmed());
}
if let Some(env) = &query.env {
println!();
println!(" {} {}", "Environment:".bright_white(), env);
}
if let Some(created) = &query.created_at {
println!();
println!(" {} {}", "Created:".dimmed(), created.dimmed());
}
println!();
println!("{}", "─".repeat(68).dimmed());
println!();
println!("{}", "Equivalent command:".bright_green());
println!();
let mut cmd = format!("mrapids run {}", query.operation);
for (key, value) in &query.params {
if value.contains(' ') || value.contains('"') {
cmd.push_str(&format!(" \\\n --param '{}={}'", key, value));
} else {
cmd.push_str(&format!(" \\\n --param {}={}", key, value));
}
}
for (key, value) in &query.headers {
cmd.push_str(&format!(" \\\n --header '{}: {}'", key, value));
}
if let Some(body) = &query.body {
cmd.push_str(&format!(" \\\n --data '{}'", body.replace('\'', "\\'")));
}
if let Some(env) = &query.env {
cmd.push_str(&format!(" \\\n --env {}", env));
}
println!(" {}", cmd.bright_cyan());
println!();
Ok(())
}
fn truncate(s: &str, max_len: usize) -> String {
if s.len() <= max_len {
s.to_string()
} else {
format!("{}...", &s[..max_len.saturating_sub(3)])
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_saved_query_from_run_params() {
let query = SavedQuery::from_run_params(
"test-query",
"users/list",
&["page=1".to_string(), "limit=10".to_string()],
&["Authorization: Bearer token".to_string()],
Some(r#"{"name": "test"}"#),
Some("production"),
);
assert_eq!(query.name, "test-query");
assert_eq!(query.operation, "users/list");
assert_eq!(query.params.get("page"), Some(&"1".to_string()));
assert_eq!(query.params.get("limit"), Some(&"10".to_string()));
assert_eq!(
query.headers.get("Authorization"),
Some(&"Bearer token".to_string())
);
assert_eq!(query.body, Some(r#"{"name": "test"}"#.to_string()));
assert_eq!(query.env, Some("production".to_string()));
assert!(query.created_at.is_some());
}
#[test]
fn test_saved_query_to_args() {
let mut params = HashMap::new();
params.insert("page".to_string(), "1".to_string());
let mut headers = HashMap::new();
headers.insert("Authorization".to_string(), "Bearer token".to_string());
let query = SavedQuery {
name: "test".to_string(),
operation: "users/list".to_string(),
params,
headers,
body: Some(r#"{"test": true}"#.to_string()),
description: None,
env: Some("dev".to_string()),
created_at: None,
};
let args = query.to_args();
assert!(args.contains(&"run".to_string()));
assert!(args.contains(&"users/list".to_string()));
assert!(args.contains(&"--param".to_string()));
assert!(args.contains(&"page=1".to_string()));
assert!(args.contains(&"--header".to_string()));
assert!(args.contains(&"Authorization: Bearer token".to_string()));
assert!(args.contains(&"--data".to_string()));
assert!(args.contains(&r#"{"test": true}"#.to_string()));
assert!(args.contains(&"--env".to_string()));
assert!(args.contains(&"dev".to_string()));
}
#[test]
fn test_truncate() {
assert_eq!(truncate("hello", 10), "hello");
assert_eq!(truncate("hello world", 8), "hello...");
assert_eq!(truncate("abc", 3), "abc");
assert_eq!(truncate("abcdef", 5), "ab...");
}
#[test]
fn test_saved_query_without_optional_fields() {
let query = SavedQuery::from_run_params("minimal", "GET /users", &[], &[], None, None);
assert_eq!(query.name, "minimal");
assert_eq!(query.operation, "GET /users");
assert!(query.params.is_empty());
assert!(query.headers.is_empty());
assert!(query.body.is_none());
assert!(query.env.is_none());
}
#[test]
fn test_saved_query_serialization() {
let mut params = HashMap::new();
params.insert("id".to_string(), "123".to_string());
let query = SavedQuery {
name: "test-ser".to_string(),
operation: "repos/get".to_string(),
params,
headers: HashMap::new(),
body: None,
description: Some("Test query".to_string()),
env: None,
created_at: None,
};
let yaml = serde_yaml::to_string(&query).unwrap();
assert!(yaml.contains("name: test-ser"));
assert!(yaml.contains("operation: repos/get"));
assert!(yaml.contains("id: '123'") || yaml.contains("id: \"123\""));
let restored: SavedQuery = serde_yaml::from_str(&yaml).unwrap();
assert_eq!(restored.name, query.name);
assert_eq!(restored.operation, query.operation);
assert_eq!(restored.params.get("id"), Some(&"123".to_string()));
}
}