use std::fs;
use std::path::Path;
use serde_json::Value;
use crate::error::RjdError;
fn extract_paths_from_value(value: &Value, prefix: &str, paths: &mut Vec<String>) {
if let Some(obj) = value.as_object() {
for (key, val) in obj {
let is_truthy = val == &Value::Bool(true)
|| (val.is_object() && !val.as_object().unwrap().is_empty())
|| val.is_number();
if is_truthy {
let path = if prefix.is_empty() {
format!("/{}", key)
} else {
format!("{}/{}", prefix, key)
};
if val.is_object() && !val.as_object().unwrap().is_empty() {
extract_paths_from_value(val, &path, paths);
} else {
paths.push(path);
}
}
}
}
}
pub fn load_ignore_patterns(path: &Path) -> Result<Vec<String>, RjdError> {
if !path.exists() {
return Err(RjdError::FileRead {
path: path.to_path_buf(),
source: std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("File not found: {}", path.display()),
),
});
}
if !path.is_file() {
return Err(RjdError::FileRead {
path: path.to_path_buf(),
source: std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Not a file: {}", path.display()),
),
});
}
let content = fs::read_to_string(path).map_err(|source| RjdError::FileRead {
path: path.to_path_buf(),
source,
})?;
let value: Value = serde_json::from_str(&content).map_err(|source| RjdError::JsonParse {
path: path.to_path_buf(),
source,
})?;
if let Some(arr) = value.as_array() {
let patterns: Vec<String> =
serde_json::from_value(Value::Array(arr.clone())).map_err(|source| {
RjdError::JsonParse {
path: path.to_path_buf(),
source,
}
})?;
for pattern in &patterns {
if !pattern.starts_with('/') {
return Err(RjdError::Internal {
message: format!(
"Ignore pattern '{}' must start with '/' (JSON Pointer format)",
pattern
),
});
}
}
return Ok(patterns);
}
if value.is_object() {
let mut patterns = Vec::new();
extract_paths_from_value(&value, "", &mut patterns);
patterns.sort();
patterns.dedup();
return Ok(patterns);
}
Err(RjdError::Internal {
message: "Ignore file must be either a JSON array of strings or a JSON object".to_string(),
})
}
pub fn load_all_ignore_patterns(paths: &[String]) -> Result<Vec<String>, RjdError> {
let mut all_patterns = Vec::new();
for path_str in paths {
let path = Path::new(path_str);
let patterns = load_ignore_patterns(path)?;
all_patterns.extend(patterns);
}
Ok(all_patterns)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::NamedTempFile;
#[test]
fn test_load_valid_patterns() {
let temp_file = NamedTempFile::new().unwrap();
let file_path = temp_file.path().to_path_buf();
drop(temp_file);
std::fs::write(&file_path, r#"["/user/id", "/config/password", "/a/b/c"]"#).unwrap();
let result = load_ignore_patterns(&file_path);
assert!(result.is_ok());
let patterns = result.unwrap();
assert_eq!(patterns.len(), 3);
assert_eq!(patterns[0], "/user/id");
}
#[test]
fn test_load_empty_array() {
let temp_file = NamedTempFile::new().unwrap();
let file_path = temp_file.path().to_path_buf();
drop(temp_file);
std::fs::write(&file_path, r#"[]"#).unwrap();
let result = load_ignore_patterns(&file_path);
assert!(result.is_ok());
assert!(result.unwrap().is_empty());
}
#[test]
fn test_load_invalid_json() {
let temp_file = NamedTempFile::new().unwrap();
let file_path = temp_file.path().to_path_buf();
drop(temp_file);
std::fs::write(&file_path, r#"not valid json"#).unwrap();
let result = load_ignore_patterns(&file_path);
assert!(result.is_err());
}
#[test]
fn test_load_object_with_string_values_ignored() {
let temp_file = NamedTempFile::new().unwrap();
let file_path = temp_file.path().to_path_buf();
drop(temp_file);
std::fs::write(&file_path, r#"{"key": "value"}"#).unwrap();
let result = load_ignore_patterns(&file_path);
assert!(result.is_ok());
assert!(result.unwrap().is_empty());
}
#[test]
fn test_load_invalid_pattern_missing_slash() {
let temp_file = NamedTempFile::new().unwrap();
let file_path = temp_file.path().to_path_buf();
drop(temp_file);
std::fs::write(&file_path, r#"["user/id", "/config/password"]"#).unwrap();
let result = load_ignore_patterns(&file_path);
assert!(result.is_err());
}
#[test]
fn test_load_nonexistent_file() {
let result = load_ignore_patterns(Path::new("/nonexistent/paths.json"));
assert!(result.is_err());
}
#[test]
fn test_load_all_patterns_multiple_files() {
let temp_file1 = NamedTempFile::new().unwrap();
let file_path1 = temp_file1.path().to_path_buf();
drop(temp_file1);
std::fs::write(&file_path1, r#"["/a/b", "/c/d"]"#).unwrap();
let temp_file2 = NamedTempFile::new().unwrap();
let file_path2 = temp_file2.path().to_path_buf();
drop(temp_file2);
std::fs::write(&file_path2, r#"["/e/f"]"#).unwrap();
let paths = vec![
file_path1.to_string_lossy().to_string(),
file_path2.to_string_lossy().to_string(),
];
let result = load_all_ignore_patterns(&paths);
assert!(result.is_ok());
let patterns = result.unwrap();
assert_eq!(patterns.len(), 3);
assert_eq!(patterns, vec!["/a/b", "/c/d", "/e/f"]);
}
#[test]
fn test_load_object_format() {
let temp_file = NamedTempFile::new().unwrap();
let file_path = temp_file.path().to_path_buf();
drop(temp_file);
std::fs::write(
&file_path,
r#"{"user": {"id": true, "name": true}, "tags": true}"#,
)
.unwrap();
let result = load_ignore_patterns(&file_path);
assert!(result.is_ok());
let patterns = result.unwrap();
assert_eq!(patterns.len(), 3);
assert!(patterns.contains(&"/user/id".to_string()));
assert!(patterns.contains(&"/user/name".to_string()));
assert!(patterns.contains(&"/tags".to_string()));
}
#[test]
fn test_load_object_nested() {
let temp_file = NamedTempFile::new().unwrap();
let file_path = temp_file.path().to_path_buf();
drop(temp_file);
std::fs::write(&file_path, r#"{"a": {"b": {"c": true}}}"#).unwrap();
let result = load_ignore_patterns(&file_path);
assert!(result.is_ok());
let patterns = result.unwrap();
assert_eq!(patterns.len(), 1);
assert!(patterns.contains(&"/a/b/c".to_string()));
}
#[test]
fn test_load_object_empty() {
let temp_file = NamedTempFile::new().unwrap();
let file_path = temp_file.path().to_path_buf();
drop(temp_file);
std::fs::write(&file_path, r#"{}"#).unwrap();
let result = load_ignore_patterns(&file_path);
assert!(result.is_ok());
assert!(result.unwrap().is_empty());
}
#[test]
fn test_load_object_false_values_ignored() {
let temp_file = NamedTempFile::new().unwrap();
let file_path = temp_file.path().to_path_buf();
drop(temp_file);
std::fs::write(
&file_path,
r#"{"user": {"id": true, "skip": false}, "tags": true}"#,
)
.unwrap();
let result = load_ignore_patterns(&file_path);
assert!(result.is_ok());
let patterns = result.unwrap();
assert_eq!(patterns.len(), 2);
assert!(patterns.contains(&"/user/id".to_string()));
assert!(patterns.contains(&"/tags".to_string()));
assert!(!patterns.contains(&"/user/skip".to_string()));
}
}