use crate::{
config::Config,
error::{Error, Result},
models::{ProjectFilter, ProjectSort},
};
use regex::Regex;
use std::fs;
use std::io::{BufRead, Write};
use std::path::PathBuf;
pub fn parse_filters(expressions: &[String]) -> Result<Vec<ProjectFilter>> {
expressions
.iter()
.map(|expression| parse_filter(expression))
.collect()
}
pub fn parse_filter(expression: &str) -> Result<ProjectFilter> {
let trimmed = expression.trim();
if trimmed.is_empty() {
return Err(Error::EmptyFilter);
}
if let Some(index) = trimmed.find("!=") {
let (field, value) = trimmed.split_at(index);
let value = &value[2..];
return Ok(ProjectFilter::NotEquals(
field.trim().to_string(),
value.trim().to_string(),
));
}
if let Some(index) = trimmed.find('~') {
let (field, pattern) = trimmed.split_at(index);
let regex = Regex::new(pattern[1..].trim())?;
return Ok(ProjectFilter::Regex(field.trim().to_string(), regex));
}
if let Some(index) = trimmed.find('=') {
let (field, value) = trimmed.split_at(index);
let value = &value[1..];
return Ok(ProjectFilter::Equals(
field.trim().to_string(),
value.trim().to_string(),
));
}
if let Some(index) = trimmed.find(':') {
let (field, value) = trimmed.split_at(index);
let value = &value[1..];
return Ok(ProjectFilter::Contains(
field.trim().to_string(),
value.trim().to_string(),
));
}
Err(Error::UnparseableFilter(trimmed.to_string()))
}
pub fn load_saved_filters(config: &Config, name: &str) -> Result<Vec<ProjectFilter>> {
let mut path = config.filters_dir();
path.push(format!("{name}.filters"));
if !path.exists() {
return Err(Error::SavedFilterNotFound {
name: name.to_owned(),
path,
});
}
let file = fs::File::open(&path)?;
let reader = std::io::BufReader::new(file);
let mut expressions = Vec::new();
for line in reader.lines() {
let line = line?;
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
expressions.push(trimmed.to_string());
}
if expressions.is_empty() {
return Err(Error::EmptySavedFilter(name.to_owned()));
}
parse_filters(&expressions)
}
pub fn parse_sort(value: Option<&str>) -> Result<Option<ProjectSort>> {
Ok(match value {
None => None,
Some(raw) => {
let normalized = raw.trim().to_ascii_lowercase();
match normalized.as_str() {
"" => None,
"name" => Some(ProjectSort::Name),
"created_at" | "created" => Some(ProjectSort::CreatedAt),
"modified_at" | "modified" => Some(ProjectSort::ModifiedAt),
other => {
return Err(Error::UnsupportedSortField(other.to_owned()));
}
}
}
})
}
pub fn ensure_filters_dir(config: &Config) -> Result<PathBuf> {
let dir = config.filters_dir();
fs::create_dir_all(&dir)?;
Ok(dir)
}
pub fn save_filters(config: &Config, name: &str, expressions: &[String]) -> Result<PathBuf> {
if expressions.is_empty() {
return Err(Error::NoFilterExpressions(name.to_owned()));
}
let dir = ensure_filters_dir(config)?;
let path = dir.join(format!("{name}.filters"));
let mut file = fs::File::create(&path)?;
for expression in expressions {
writeln!(file, "{expression}")?;
}
Ok(path)
}