#![allow(dead_code)]
use super::models::Collection;
use crate::core::parser::UnifiedSpec;
use std::collections::HashSet;
#[derive(Debug, Default)]
pub struct ValidationResult {
pub errors: Vec<String>,
pub warnings: Vec<String>,
}
impl ValidationResult {
pub fn is_valid(&self) -> bool {
self.errors.is_empty()
}
pub fn add_error(&mut self, error: String) {
self.errors.push(error);
}
pub fn add_warning(&mut self, warning: String) {
self.warnings.push(warning);
}
}
pub fn validate_collection(
collection: &Collection,
spec: Option<&UnifiedSpec>,
) -> ValidationResult {
let mut result = ValidationResult::default();
validate_structure(collection, &mut result);
if let Some(spec) = spec {
validate_against_spec(collection, spec, &mut result);
}
validate_variables(collection, &mut result);
result
}
fn validate_structure(collection: &Collection, result: &mut ValidationResult) {
if collection.name.is_empty() {
result.add_error("Collection name cannot be empty".to_string());
}
if collection.requests.is_empty() {
result.add_error("Collection must contain at least one request".to_string());
}
let mut seen_names = HashSet::new();
for request in &collection.requests {
if !seen_names.insert(&request.name) {
result.add_error(format!("Duplicate request name: {}", request.name));
}
if request.name.is_empty() {
result.add_error("Request name cannot be empty".to_string());
}
if request.operation.is_empty() {
result.add_error(format!(
"Operation cannot be empty for request '{}'",
request.name
));
}
}
}
fn validate_against_spec(
collection: &Collection,
spec: &UnifiedSpec,
result: &mut ValidationResult,
) {
let available_ops: HashSet<&str> = spec
.operations
.iter()
.map(|op| op.operation_id.as_str())
.collect();
for request in &collection.requests {
if !available_ops.contains(request.operation.as_str()) {
result.add_error(format!(
"Unknown operation '{}' in request '{}'",
request.operation, request.name
));
let similar = find_similar_operations(&request.operation, &available_ops);
if !similar.is_empty() {
result.add_warning(format!("Did you mean one of: {}?", similar.join(", ")));
}
}
}
}
fn validate_variables(collection: &Collection, result: &mut ValidationResult) {
let mut used_vars = HashSet::new();
let mut saved_vars = HashSet::new();
for request in &collection.requests {
if let Some(save_as) = &request.save_as {
if saved_vars.contains(save_as) {
result.add_warning(format!("Variable '{}' is saved multiple times", save_as));
}
saved_vars.insert(save_as.clone());
}
if let Some(params) = &request.params {
for value in params.values() {
extract_template_vars(value, &mut used_vars);
}
}
if let Some(body) = &request.body {
extract_template_vars(body, &mut used_vars);
}
}
for var in &used_vars {
if var.contains('.') {
let prefix = var.split('.').next().unwrap();
if saved_vars.contains(prefix) {
continue;
}
}
if !collection.variables.contains_key(var) && !saved_vars.contains(var) {
result.add_warning(format!(
"Variable '{}' is used but not defined in collection",
var
));
}
}
}
fn extract_template_vars(value: &serde_json::Value, vars: &mut HashSet<String>) {
match value {
serde_json::Value::String(s) => {
let re = regex::Regex::new(r"\{\{([^}]+)\}\}").unwrap();
for cap in re.captures_iter(s) {
if let Some(var) = cap.get(1) {
vars.insert(var.as_str().trim().to_string());
}
}
}
serde_json::Value::Object(map) => {
for v in map.values() {
extract_template_vars(v, vars);
}
}
serde_json::Value::Array(arr) => {
for v in arr {
extract_template_vars(v, vars);
}
}
_ => {}
}
}
fn find_similar_operations(target: &str, available: &HashSet<&str>) -> Vec<String> {
let mut similar = Vec::new();
for op in available {
if op.contains(&target) || target.contains(op) {
similar.push(op.to_string());
}
}
similar.sort();
similar.truncate(3);
similar
}
#[cfg(test)]
mod tests {
use super::*;
use crate::collections::models::CollectionRequest;
#[test]
fn test_validate_empty_collection() {
let collection = Collection {
name: "test".to_string(),
description: None,
requests: vec![],
variables: Default::default(),
auth_profile: None,
};
let result = validate_collection(&collection, None);
assert!(!result.is_valid());
assert!(result
.errors
.iter()
.any(|e| e.contains("at least one request")));
}
#[test]
fn test_validate_duplicate_names() {
let collection = Collection {
name: "test".to_string(),
description: None,
requests: vec![
CollectionRequest {
name: "req1".to_string(),
operation: "op1".to_string(),
params: None,
body: None,
save_as: None,
expect: None,
depends_on: None,
if_condition: None,
skip: None,
run_always: false,
critical: false,
retry: None,
},
CollectionRequest {
name: "req1".to_string(),
operation: "op2".to_string(),
params: None,
body: None,
save_as: None,
expect: None,
depends_on: None,
if_condition: None,
skip: None,
run_always: false,
critical: false,
retry: None,
},
],
variables: Default::default(),
auth_profile: None,
};
let result = validate_collection(&collection, None);
assert!(!result.is_valid());
assert!(result
.errors
.iter()
.any(|e| e.contains("Duplicate request name")));
}
}