use super::types::{DryRunError, InputValidation, JsonPathValidation};
use serde_json::Value;
use std::collections::HashMap;
use std::path::Path;
use tokio::fs;
use tracing::{debug, warn};
pub struct InputValidator;
impl InputValidator {
pub fn new() -> Self {
Self
}
pub async fn validate_input_source(&self, input: &str) -> Result<InputValidation, DryRunError> {
debug!("Validating input source: {}", input);
if input.starts_with("shell:") {
self.validate_command_input(input).await
} else if Path::new(input).exists() {
self.validate_file_input(input).await
} else {
Ok(InputValidation {
source: input.to_string(),
valid: false,
size_bytes: 0,
item_count_estimate: 0,
data_structure: "unknown".to_string(),
})
}
}
async fn validate_file_input(&self, path: &str) -> Result<InputValidation, DryRunError> {
debug!("Validating file input: {}", path);
let content = fs::read_to_string(path).await.map_err(|e| {
DryRunError::InputError(format!("Failed to read input file {}: {}", path, e))
})?;
let data: Value = serde_json::from_str(&content).map_err(DryRunError::JsonError)?;
let item_count = self.estimate_item_count(&data);
let structure = self.analyze_structure(&data);
Ok(InputValidation {
source: path.to_string(),
valid: true,
size_bytes: content.len(),
item_count_estimate: item_count,
data_structure: structure,
})
}
async fn validate_command_input(&self, command: &str) -> Result<InputValidation, DryRunError> {
debug!("Validating command input: {}", command);
let cmd = command.strip_prefix("shell:").unwrap_or(command).trim();
if cmd.is_empty() {
return Ok(InputValidation {
source: command.to_string(),
valid: false,
size_bytes: 0,
item_count_estimate: 0,
data_structure: "invalid command".to_string(),
});
}
let dangerous_commands = ["rm", "del", "format", "dd", "mkfs"];
let first_word = cmd.split_whitespace().next().unwrap_or("");
if dangerous_commands.contains(&first_word) {
warn!("Potentially dangerous command detected: {}", cmd);
}
Ok(InputValidation {
source: command.to_string(),
valid: true,
size_bytes: 0, item_count_estimate: 0, data_structure: "command output (not executed in dry-run)".to_string(),
})
}
pub async fn load_work_items(
&self,
input: &str,
json_path: Option<&str>,
) -> Result<Vec<Value>, DryRunError> {
if input.starts_with("shell:") {
warn!("Command input in dry-run mode, returning empty work items");
return Ok(Vec::new());
}
if !Path::new(input).exists() {
return Err(DryRunError::InputError(format!(
"Input file does not exist: {}",
input
)));
}
let content = fs::read_to_string(input)
.await
.map_err(|e| DryRunError::InputError(format!("Failed to read input file: {}", e)))?;
let data: Value = serde_json::from_str(&content)?;
if let Some(path) = json_path {
self.extract_with_jsonpath(&data, path)
} else if let Value::Array(items) = data {
Ok(items)
} else {
Ok(vec![data])
}
}
pub async fn validate_jsonpath(
&self,
path: &str,
sample_data: &Value,
) -> Result<JsonPathValidation, DryRunError> {
debug!("Validating JSONPath: {}", path);
if path.is_empty() {
return Err(DryRunError::JsonPathError("Empty JSONPath".to_string()));
}
let match_values = self.extract_with_jsonpath(sample_data, path)?;
let data_types = self.analyze_data_types(&match_values);
Ok(JsonPathValidation {
path: path.to_string(),
valid: true,
match_count: match_values.len(),
sample_matches: match_values.into_iter().take(5).collect(),
data_types,
})
}
fn extract_with_jsonpath(&self, data: &Value, path: &str) -> Result<Vec<Value>, DryRunError> {
if path == "$" {
return Ok(vec![data.clone()]);
}
if path == "$.items[*]" || path == "$.items" {
if let Value::Object(obj) = data {
if let Some(Value::Array(items)) = obj.get("items") {
return Ok(items.clone());
}
}
}
if path == "$[*]" {
if let Value::Array(items) = data {
return Ok(items.clone());
}
}
if let Some(field) = path.strip_prefix("$.") {
if !field.contains('[') && !field.contains('.') {
if let Value::Object(obj) = data {
if let Some(value) = obj.get(field) {
if let Value::Array(items) = value {
return Ok(items.clone());
}
return Ok(vec![value.clone()]);
}
}
}
}
warn!(
"Complex JSONPath '{}' not fully supported in dry-run mode",
path
);
Ok(vec![data.clone()])
}
fn estimate_item_count(&self, data: &Value) -> usize {
match data {
Value::Array(items) => items.len(),
Value::Object(map) => {
for key in ["items", "data", "results", "records"] {
if let Some(Value::Array(items)) = map.get(key) {
return items.len();
}
}
1 }
_ => 1,
}
}
fn analyze_structure(&self, data: &Value) -> String {
match data {
Value::Array(items) => {
if items.is_empty() {
"empty array".to_string()
} else {
format!("array of {} items", items.len())
}
}
Value::Object(map) => {
let keys: Vec<String> = map.keys().take(5).cloned().collect();
if keys.is_empty() {
"empty object".to_string()
} else {
format!("object with keys: {}", keys.join(", "))
}
}
Value::String(_) => "string".to_string(),
Value::Number(_) => "number".to_string(),
Value::Bool(_) => "boolean".to_string(),
Value::Null => "null".to_string(),
}
}
fn analyze_data_types(&self, values: &[Value]) -> HashMap<String, usize> {
let mut types = HashMap::new();
for value in values {
let type_name = match value {
Value::Null => "null",
Value::Bool(_) => "boolean",
Value::Number(_) => "number",
Value::String(_) => "string",
Value::Array(_) => "array",
Value::Object(_) => "object",
};
*types.entry(type_name.to_string()).or_insert(0) += 1;
}
types
}
}
impl Default for InputValidator {
fn default() -> Self {
Self::new()
}
}