use super::{MDItemKey, MDQuery, MDQueryScope};
use anyhow::Result;
#[derive(Default)]
pub struct MDQueryBuilder {
condition: MDQueryCondition,
}
impl MDQueryBuilder {
pub fn build(self, scopes: Vec<MDQueryScope>, max_count: Option<usize>) -> Result<MDQuery> {
if self.condition.is_empty() {
anyhow::bail!("No expressions to build");
}
let query = self.condition.into_expression();
MDQuery::new(&query, Some(scopes), max_count)
}
pub fn from_condition(condition: MDQueryCondition) -> Self {
Self { condition }
}
pub fn from_raw(query: &str) -> Self {
let mut condition = MDQueryCondition::default();
condition.add(MDQueryConditionExpression::Expression(query.to_string()));
Self { condition }
}
pub fn name_like(mut self, name: &str) -> Self {
self.condition
.add(MDQueryConditionExpression::Expression(format!(
"{} == \"*{}*\"w",
MDItemKey::DisplayName,
name
)));
self
}
pub fn name_is(mut self, name: &str) -> Self {
self.condition
.add(MDQueryConditionExpression::Expression(format!(
"{} == \"{}\"c",
MDItemKey::DisplayName,
name
)));
self
}
pub fn time(mut self, key: MDItemKey, op: MDQueryCompareOp, timestamp: i64) -> Self {
if !key.is_time() {
panic!("Cannot use time on non-time key");
}
let time_str = chrono::DateTime::from_timestamp(timestamp, 0)
.unwrap()
.to_rfc3339();
self.condition
.add(MDQueryConditionExpression::Expression(format!(
"{} {} $time.iso({})",
key,
op.into_query_string(),
time_str
)));
self
}
pub fn size(mut self, op: MDQueryCompareOp, size: u64) -> Self {
self.condition
.add(MDQueryConditionExpression::Expression(format!(
"{} {} {}",
MDItemKey::Size,
op.into_query_string(),
size
)));
self
}
pub fn is_dir(mut self, value: bool) -> Self {
self.condition
.add(MDQueryConditionExpression::Expression(format!(
"{} {} \"{}\"",
MDItemKey::ContentType,
if value { "==" } else { "!=" },
"public.folder"
)));
self
}
pub fn is_app(self) -> Self {
self.content_type("com.apple.application-bundle")
}
pub fn extension(mut self, ext: &str) -> Self {
self.condition
.add(MDQueryConditionExpression::Expression(format!(
"{} == \"*.{}\"c",
MDItemKey::FSName,
ext
)));
self
}
pub fn content_type(mut self, content_type: &str) -> Self {
self.condition
.add(MDQueryConditionExpression::Expression(format!(
"{} == \"{}\"",
MDItemKey::ContentType,
content_type
)));
self
}
}
pub struct MDQueryCondition {
condition_type: MDQueryConditionType,
expressions: Vec<MDQueryConditionExpression>,
}
impl Default for MDQueryCondition {
fn default() -> Self {
Self {
condition_type: MDQueryConditionType::All,
expressions: Vec::new(),
}
}
}
impl MDQueryCondition {
pub fn into_expression(self) -> String {
let expr = match self.condition_type {
MDQueryConditionType::All => self
.expressions
.into_iter()
.map(|e| e.into_expression())
.collect::<Vec<_>>()
.join(" && "),
MDQueryConditionType::Any => self
.expressions
.into_iter()
.map(|e| e.into_expression())
.collect::<Vec<_>>()
.join(" || "),
};
format!("({})", expr)
}
pub fn add(&mut self, expr: MDQueryConditionExpression) {
self.expressions.push(expr);
}
pub fn is_empty(&self) -> bool {
self.expressions.is_empty()
}
}
pub enum MDQueryConditionType {
All,
Any,
}
pub enum MDQueryConditionExpression {
Condition(MDQueryCondition),
Expression(String),
}
impl MDQueryConditionExpression {
pub fn into_expression(self) -> String {
match self {
Self::Condition(c) => c.into_expression(),
Self::Expression(e) => format!("({})", e),
}
}
}
pub enum MDQueryCompareOp {
GreaterThan,
LessThan,
Equal,
GreaterThanOrEqual,
LessThanOrEqual,
}
impl MDQueryCompareOp {
fn into_query_string(self) -> &'static str {
match self {
Self::GreaterThan => ">",
Self::LessThan => "<",
Self::Equal => "==",
Self::GreaterThanOrEqual => ">=",
Self::LessThanOrEqual => "<=",
}
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::*;
#[test]
fn test_name_like() {
let builder = MDQueryBuilder::default().name_like("Safari");
let query = builder
.build(vec![MDQueryScope::Computer], Some(1))
.unwrap();
let results = query.execute().unwrap();
assert_eq!(results.len(), 1);
assert_eq!(
results[0].path(),
Some(PathBuf::from("/Applications/Safari.app"))
);
}
#[test]
fn test_is_app() {
let builder = MDQueryBuilder::default().name_like("Safari").is_app();
let query = builder
.build(vec![MDQueryScope::Computer], Some(1))
.unwrap();
let results = query.execute().unwrap();
assert_eq!(results.len(), 1);
assert_eq!(
results[0].path(),
Some(PathBuf::from("/Applications/Safari.app"))
);
}
#[test]
fn test_extension() {
let builder = MDQueryBuilder::default().extension("txt");
let query = builder
.build(vec![MDQueryScope::Computer], Some(1))
.unwrap();
let results = query.execute().unwrap();
assert!(!results.is_empty());
assert!(results[0]
.path()
.unwrap()
.to_str()
.unwrap()
.ends_with(".txt"));
}
#[test]
fn test_time_search() {
let now = chrono::Utc::now().timestamp();
let builder = MDQueryBuilder::default().time(
MDItemKey::ModificationDate,
MDQueryCompareOp::LessThan,
now,
);
let query = builder
.build(vec![MDQueryScope::from_path("/Applications")], Some(1))
.unwrap();
let results = query.execute().unwrap();
assert!(!results.is_empty());
}
#[test]
fn test_size_filter() {
let builder = MDQueryBuilder::default().size(MDQueryCompareOp::GreaterThan, 1024 * 1024); let query = builder
.build(vec![MDQueryScope::Computer], Some(1))
.unwrap();
let results = query.execute().unwrap();
assert!(!results.is_empty());
}
#[test]
fn test_is_dir() {
let builder = MDQueryBuilder::default().is_dir(true);
let query = builder
.build(vec![MDQueryScope::Computer], Some(1))
.unwrap();
let results = query.execute().unwrap();
assert!(!results.is_empty());
}
#[test]
fn test_condition_all() {
let condition = MDQueryCondition {
condition_type: MDQueryConditionType::All,
expressions: vec![
MDQueryConditionExpression::Expression("kMDItemFSName == \"test.txt\"".into()),
MDQueryConditionExpression::Expression("kMDItemTextContent == \"hello\"".into()),
],
};
assert_eq!(
condition.into_expression(),
"((kMDItemFSName == \"test.txt\") && (kMDItemTextContent == \"hello\"))"
);
}
#[test]
fn test_condition_any() {
let condition = MDQueryCondition {
condition_type: MDQueryConditionType::Any,
expressions: vec![
MDQueryConditionExpression::Expression("kMDItemFSName == \"doc.pdf\"".into()),
MDQueryConditionExpression::Expression("kMDItemFSName == \"doc.txt\"".into()),
],
};
assert_eq!(
condition.into_expression(),
"((kMDItemFSName == \"doc.pdf\") || (kMDItemFSName == \"doc.txt\"))"
);
}
#[test]
fn test_nested_condition() {
let inner_condition = MDQueryCondition {
condition_type: MDQueryConditionType::Any,
expressions: vec![
MDQueryConditionExpression::Expression("kMDItemFSName == \"*.txt\"".into()),
MDQueryConditionExpression::Expression("kMDItemFSName == \"*.pdf\"".into()),
],
};
let outer_condition = MDQueryCondition {
condition_type: MDQueryConditionType::All,
expressions: vec![
MDQueryConditionExpression::Condition(inner_condition),
MDQueryConditionExpression::Expression("kMDItemTextContent == \"test\"".into()),
],
};
assert_eq!(
outer_condition.into_expression(),
"(((kMDItemFSName == \"*.txt\") || (kMDItemFSName == \"*.pdf\")) && (kMDItemTextContent == \"test\"))"
);
}
#[test]
fn test_empty_condition() {
let condition = MDQueryCondition {
condition_type: MDQueryConditionType::All,
expressions: vec![],
};
assert_eq!(condition.into_expression(), "()");
}
}