srusty-files 0.2.0

A high-performance, cross-platform file search engine library with REST API
Documentation
use crate::core::error::{Result, SearchError};
use crate::core::types::{DateFilter, MatchMode, SearchScope, SizeFilter};
use crate::filters::{parse_relative_date, parse_size};

#[derive(Debug, Clone)]
pub struct Query {
    pub pattern: String,
    pub match_mode: MatchMode,
    pub scope: SearchScope,
    pub size_filter: Option<SizeFilter>,
    pub date_filter: Option<DateFilter>,
    pub extensions: Vec<String>,
    pub max_results: Option<usize>,
}

impl Query {
    pub fn new(pattern: String) -> Self {
        Self {
            pattern,
            match_mode: MatchMode::CaseInsensitive,
            scope: SearchScope::Name,
            size_filter: None,
            date_filter: None,
            extensions: Vec::new(),
            max_results: None,
        }
    }

    pub fn with_match_mode(mut self, mode: MatchMode) -> Self {
        self.match_mode = mode;
        self
    }

    pub fn with_scope(mut self, scope: SearchScope) -> Self {
        self.scope = scope;
        self
    }

    pub fn with_size_filter(mut self, filter: SizeFilter) -> Self {
        self.size_filter = Some(filter);
        self
    }

    pub fn with_date_filter(mut self, filter: DateFilter) -> Self {
        self.date_filter = Some(filter);
        self
    }

    pub fn with_extensions(mut self, extensions: Vec<String>) -> Self {
        self.extensions = extensions;
        self
    }

    pub fn with_max_results(mut self, max: usize) -> Self {
        self.max_results = Some(max);
        self
    }
}

pub struct QueryParser;

impl QueryParser {
    pub fn parse(input: &str) -> Result<Query> {
        let mut query = Query::new(String::new());
        let parts: Vec<&str> = input.split_whitespace().collect();

        let mut pattern_parts = Vec::new();
        let mut i = 0;

        while i < parts.len() {
            let part = parts[i];

            if part.contains(':') {
                let (key, value) = part.split_once(':').unwrap();
                match key.to_lowercase().as_str() {
                    "ext" | "extension" => {
                        query.extensions = value.split(',').map(|s| s.to_string()).collect();
                    }
                    "size" => {
                        query.size_filter = Self::parse_size_filter(value)?;
                    }
                    "modified" | "date" => {
                        query.date_filter = Self::parse_date_filter(value)?;
                    }
                    "mode" => {
                        query.match_mode = Self::parse_match_mode(value)?;
                    }
                    "scope" => {
                        query.scope = Self::parse_scope(value)?;
                    }
                    "limit" | "max" => {
                        if let Ok(max) = value.parse::<usize>() {
                            query.max_results = Some(max);
                        }
                    }
                    _ => {
                        pattern_parts.push(part);
                    }
                }
            } else {
                pattern_parts.push(part);
            }

            i += 1;
        }

        query.pattern = pattern_parts.join(" ");

        if query.pattern.is_empty() {
            return Err(SearchError::InvalidQuery(
                "Query pattern cannot be empty".to_string(),
            ));
        }

        Ok(query)
    }

    fn parse_size_filter(value: &str) -> Result<Option<SizeFilter>> {
        if value.starts_with('>') {
            let size_str = value.trim_start_matches('>');
            if let Some(size) = parse_size(size_str) {
                return Ok(Some(SizeFilter::GreaterThan(size)));
            }
        } else if value.starts_with('<') {
            let size_str = value.trim_start_matches('<');
            if let Some(size) = parse_size(size_str) {
                return Ok(Some(SizeFilter::LessThan(size)));
            }
        } else if value.contains("..") {
            let parts: Vec<&str> = value.split("..").collect();
            if parts.len() == 2 {
                if let (Some(min), Some(max)) = (parse_size(parts[0]), parse_size(parts[1])) {
                    return Ok(Some(SizeFilter::Range(min, max)));
                }
            }
        } else if let Some(size) = parse_size(value) {
            return Ok(Some(SizeFilter::Exact(size)));
        }

        Err(SearchError::InvalidQuery(format!(
            "Invalid size filter: {}",
            value
        )))
    }

    fn parse_date_filter(value: &str) -> Result<Option<DateFilter>> {
        if value.starts_with('>') || value.starts_with("after:") {
            let date_str = value.trim_start_matches('>').trim_start_matches("after:");
            if let Some(date) = parse_relative_date(date_str) {
                return Ok(Some(DateFilter::After(date)));
            }
        } else if value.starts_with('<') || value.starts_with("before:") {
            let date_str = value.trim_start_matches('<').trim_start_matches("before:");
            if let Some(date) = parse_relative_date(date_str) {
                return Ok(Some(DateFilter::Before(date)));
            }
        } else if value.contains("..") {
            let parts: Vec<&str> = value.split("..").collect();
            if parts.len() == 2 {
                if let (Some(start), Some(end)) =
                    (parse_relative_date(parts[0]), parse_relative_date(parts[1]))
                {
                    return Ok(Some(DateFilter::Between(start, end)));
                }
            }
        } else if let Some(date) = parse_relative_date(value) {
            return Ok(Some(DateFilter::On(date)));
        }

        Err(SearchError::InvalidQuery(format!(
            "Invalid date filter: {}",
            value
        )))
    }

    fn parse_match_mode(value: &str) -> Result<MatchMode> {
        match value.to_lowercase().as_str() {
            "exact" => Ok(MatchMode::Exact),
            "case" | "casesensitive" => Ok(MatchMode::Exact),
            "insensitive" | "caseinsensitive" => Ok(MatchMode::CaseInsensitive),
            "fuzzy" => Ok(MatchMode::Fuzzy),
            "regex" => Ok(MatchMode::Regex),
            "glob" => Ok(MatchMode::Glob),
            _ => Err(SearchError::InvalidQuery(format!(
                "Invalid match mode: {}",
                value
            ))),
        }
    }

    fn parse_scope(value: &str) -> Result<SearchScope> {
        match value.to_lowercase().as_str() {
            "name" => Ok(SearchScope::Name),
            "path" => Ok(SearchScope::Path),
            "content" => Ok(SearchScope::Content),
            "all" => Ok(SearchScope::All),
            _ => Err(SearchError::InvalidQuery(format!(
                "Invalid search scope: {}",
                value
            ))),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_simple_query() {
        let query = QueryParser::parse("test.txt").unwrap();
        assert_eq!(query.pattern, "test.txt");
        assert_eq!(query.match_mode, MatchMode::CaseInsensitive);
    }

    #[test]
    fn test_parse_query_with_extension() {
        let query = QueryParser::parse("test ext:rs").unwrap();
        assert_eq!(query.pattern, "test");
        assert_eq!(query.extensions, vec!["rs"]);
    }

    #[test]
    fn test_parse_query_with_size() {
        let query = QueryParser::parse("test size:>1MB").unwrap();
        assert_eq!(query.pattern, "test");
        assert!(query.size_filter.is_some());
    }

    #[test]
    fn test_parse_query_with_date() {
        let query = QueryParser::parse("test modified:today").unwrap();
        assert_eq!(query.pattern, "test");
        assert!(query.date_filter.is_some());
    }

    #[test]
    fn test_parse_query_with_mode() {
        let query = QueryParser::parse("test mode:fuzzy").unwrap();
        assert_eq!(query.pattern, "test");
        assert_eq!(query.match_mode, MatchMode::Fuzzy);
    }

    #[test]
    fn test_parse_complex_query() {
        let query = QueryParser::parse("test ext:rs,txt size:>100KB modified:today mode:fuzzy").unwrap();
        assert_eq!(query.pattern, "test");
        assert_eq!(query.extensions.len(), 2);
        assert!(query.size_filter.is_some());
        assert!(query.date_filter.is_some());
        assert_eq!(query.match_mode, MatchMode::Fuzzy);
    }
}